Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a1511f1ec | |||
| d798eff5c6 | |||
|
|
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 | ||
|
|
72a5960d40 | ||
|
|
3429f1e4f9 | ||
|
|
f8bea8bf81 | ||
|
|
43794dd223 | ||
|
|
5cc84a7b46 | ||
|
|
48248a4386 | ||
|
|
95305402e6 | ||
|
|
29f24c527f | ||
|
|
efa2bbf6cc | ||
|
|
66ed9ba699 | ||
|
|
e841e64f78 | ||
|
|
b0f46803c5 | ||
|
|
ac0bef5de0 | ||
|
|
08ea40a08e | ||
|
|
1d8e8059cd | ||
|
|
20b4396a4c | ||
|
|
4c39cb4d06 | ||
|
|
f58e6abd9f | ||
|
|
4f1e83a7d7 | ||
|
|
e39b38c2d5 | ||
|
|
b9ffa6ce91 | ||
|
|
76c6753785 | ||
|
|
54f5062377 | ||
|
|
7e60fd9e40 | ||
|
|
926947bae4 | ||
|
|
2652e47018 | ||
|
|
c1500b6ed0 | ||
|
|
125bc44d2e | ||
|
|
c317c0a058 | ||
|
|
beb9498399 | ||
|
|
4e74940684 | ||
|
|
c0f73080d2 | ||
|
|
cd0a35537b |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -8,6 +8,8 @@ Ideally, the Issues tab should only consist of bug reports and feature requests
|
||||
## Feature Requests
|
||||
Feature Requests are cool, but we do want to avoid bloat and scope/feature creep.
|
||||
|
||||
Read [this](https://github.com/hargata/lubelog/wiki/Scope-and-Purpose) to better understand the scope and purpose of this project.
|
||||
|
||||
LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker.
|
||||
It is not and should not be used for the following:
|
||||
- Project Management Software(e.g.: Jira)
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug
|
||||
title: ''
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
@@ -18,7 +18,7 @@ Please make sure you have performed the following steps before opening a new bug
|
||||
|
||||
**Platform**
|
||||
- [ ] Docker Image
|
||||
- [ ] Windows Standalone Executable
|
||||
- [ ] Windows/Linux Standalone Executable
|
||||
|
||||
**Browser Console Errors(F12)**
|
||||
<!-- Attach a screenshot or codeblock containing the browser console error -->
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE REQUEST]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Checklist**
|
||||
Please make sure you have performed the following steps before submitting a new feature request, change `[ ]` to `[x]` to mark it as done
|
||||
|
||||
- [ ] I have read the [Contributing Guidelines](https://github.com/hargata/lubelog/blob/main/.github/CONTRIBUTING.md) and [Scope and Purpose](https://github.com/hargata/lubelog/wiki/Scope-and-Purpose)
|
||||
- [ ] I have searched through existing issues.
|
||||
|
||||
**Description**
|
||||
<!-- Describe the feature request below this line -->
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace CarCareTracker.Controllers
|
||||
IUserLogic userLogic,
|
||||
IVehicleLogic vehicleLogic,
|
||||
IOdometerLogic odometerLogic,
|
||||
IWebHostEnvironment webEnv)
|
||||
IWebHostEnvironment webEnv)
|
||||
{
|
||||
_dataAccess = dataAccess;
|
||||
_noteDataAccess = noteDataAccess;
|
||||
@@ -92,6 +92,26 @@ namespace CarCareTracker.Controllers
|
||||
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/whoami")]
|
||||
public IActionResult WhoAmI()
|
||||
{
|
||||
var result = new UserExportModel
|
||||
{
|
||||
Username = User.FindFirstValue(ClaimTypes.Name),
|
||||
EmailAddress = User.IsInRole(nameof(UserData.IsRootUser)) ? _config.GetUserConfig(User).DefaultReminderEmail : User.FindFirstValue(ClaimTypes.Email),
|
||||
IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)).ToString(),
|
||||
IsRoot = User.IsInRole(nameof(UserData.IsRootUser)).ToString()
|
||||
};
|
||||
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
|
||||
{
|
||||
return Json(result, StaticHelper.GetInvariantOption());
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicles")]
|
||||
public IActionResult Vehicles()
|
||||
{
|
||||
@@ -100,7 +120,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]
|
||||
@@ -153,6 +180,227 @@ namespace CarCareTracker.Controllers
|
||||
return Json(convertedOdometer);
|
||||
}
|
||||
}
|
||||
#region PlanRecord
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/planrecords")]
|
||||
public IActionResult PlanRecords(int vehicleId)
|
||||
{
|
||||
if (vehicleId == default)
|
||||
{
|
||||
var response = OperationResponse.Failed("Must provide a valid vehicle id");
|
||||
Response.StatusCode = 400;
|
||||
return Json(response);
|
||||
}
|
||||
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
|
||||
var result = vehicleRecords.Select(x => new PlanRecordExportModel {
|
||||
Id = x.Id.ToString(),
|
||||
DateCreated = x.DateCreated.ToShortDateString(),
|
||||
DateModified = x.DateModified.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString(),
|
||||
Notes = x.Notes,
|
||||
Type = x.ImportMode.ToString(),
|
||||
Priority = x.Priority.ToString(),
|
||||
Progress = x.Progress.ToString(),
|
||||
ExtraFields = x.ExtraFields,
|
||||
Files = x.Files });
|
||||
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
|
||||
{
|
||||
return Json(result, StaticHelper.GetInvariantOption());
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
[Route("/api/vehicle/planrecords/add")]
|
||||
[Consumes("application/json")]
|
||||
public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input);
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
[Route("/api/vehicle/planrecords/add")]
|
||||
public IActionResult AddPlanRecord(int vehicleId, PlanRecordExportModel 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.Cost) ||
|
||||
string.IsNullOrWhiteSpace(input.Type) ||
|
||||
string.IsNullOrWhiteSpace(input.Priority) ||
|
||||
string.IsNullOrWhiteSpace(input.Progress))
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, Description, Cost, Type, Priority, and Progress cannot be empty."));
|
||||
}
|
||||
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
|
||||
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
|
||||
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
|
||||
if (!validType || !validPriority || !validProgress)
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
|
||||
}
|
||||
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
|
||||
}
|
||||
if (parsedProgress == PlanProgress.Done)
|
||||
{
|
||||
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()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
DateCreated = DateTime.Now,
|
||||
DateModified = DateTime.Now,
|
||||
Description = input.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
|
||||
Cost = decimal.Parse(input.Cost),
|
||||
ImportMode = parsedType,
|
||||
Priority = parsedPriority,
|
||||
Progress = parsedProgress,
|
||||
ExtraFields = input.ExtraFields,
|
||||
Files = input.Files
|
||||
};
|
||||
_planRecordDataAccess.SavePlanRecordToVehicle(planRecord);
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord, "planrecord.add.api", User.Identity.Name));
|
||||
return Json(OperationResponse.Succeed("Plan Record Added"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Response.StatusCode = 500;
|
||||
return Json(OperationResponse.Failed(ex.Message));
|
||||
}
|
||||
}
|
||||
[HttpDelete]
|
||||
[Route("/api/vehicle/planrecords/delete")]
|
||||
public IActionResult DeletePlanRecord(int id)
|
||||
{
|
||||
var existingRecord = _planRecordDataAccess.GetPlanRecordById(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."));
|
||||
}
|
||||
//restore any requisitioned supplies.
|
||||
if (existingRecord.RequisitionHistory.Any())
|
||||
{
|
||||
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
|
||||
}
|
||||
var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete.api", User.Identity.Name));
|
||||
}
|
||||
return Json(OperationResponse.Conditional(result, "Plan Record Deleted"));
|
||||
}
|
||||
[HttpPut]
|
||||
[Route("/api/vehicle/planrecords/update")]
|
||||
[Consumes("application/json")]
|
||||
public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input);
|
||||
[HttpPut]
|
||||
[Route("/api/vehicle/planrecords/update")]
|
||||
public IActionResult UpdatePlanRecord(PlanRecordExportModel input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Id) ||
|
||||
string.IsNullOrWhiteSpace(input.Description) ||
|
||||
string.IsNullOrWhiteSpace(input.Cost) ||
|
||||
string.IsNullOrWhiteSpace(input.Type) ||
|
||||
string.IsNullOrWhiteSpace(input.Priority) ||
|
||||
string.IsNullOrWhiteSpace(input.Progress))
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, Id, Description, Cost, Type, Priority, and Progress cannot be empty."));
|
||||
}
|
||||
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
|
||||
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
|
||||
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
|
||||
if (!validType || !validPriority || !validProgress)
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
|
||||
}
|
||||
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
|
||||
}
|
||||
if (parsedProgress == PlanProgress.Done)
|
||||
{
|
||||
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
|
||||
var existingRecord = _planRecordDataAccess.GetPlanRecordById(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.DateModified = DateTime.Now;
|
||||
existingRecord.Description = input.Description;
|
||||
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
|
||||
existingRecord.Cost = decimal.Parse(input.Cost);
|
||||
existingRecord.ImportMode = parsedType;
|
||||
existingRecord.Priority = parsedPriority;
|
||||
existingRecord.Progress = parsedProgress;
|
||||
existingRecord.Files = input.Files;
|
||||
existingRecord.ExtraFields = input.ExtraFields;
|
||||
_planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.update.api", User.Identity.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
return Json(OperationResponse.Failed("Invalid Record Id"));
|
||||
}
|
||||
return Json(OperationResponse.Succeed("Plan Record Updated"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Response.StatusCode = 500;
|
||||
return Json(OperationResponse.Failed(ex.Message));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region ServiceRecord
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
@@ -198,6 +446,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()
|
||||
@@ -278,6 +534,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
|
||||
@@ -360,6 +624,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()
|
||||
@@ -441,6 +713,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
|
||||
@@ -524,6 +804,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()
|
||||
@@ -604,6 +892,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
|
||||
@@ -720,6 +1016,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()
|
||||
@@ -783,6 +1087,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
|
||||
@@ -882,6 +1194,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()
|
||||
@@ -943,6 +1263,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
|
||||
@@ -1042,6 +1370,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()
|
||||
@@ -1121,6 +1457,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
|
||||
@@ -1160,6 +1504,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region ReminderRecord
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/reminders")]
|
||||
@@ -1172,7 +1517,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());
|
||||
@@ -1182,6 +1527,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()
|
||||
@@ -1195,6 +1717,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)
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
public IActionResult Unauthorized()
|
||||
{
|
||||
if (!User.IsInRole("CookieAuth"))
|
||||
if (User.IsInRole("APIAuth"))
|
||||
{
|
||||
Response.StatusCode = 403;
|
||||
return new EmptyResult();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,176 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
return PartialView("_BulkDataImporter", mode);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GenerateCsvSample(ImportMode mode)
|
||||
{
|
||||
string uploadDirectory = "temp/";
|
||||
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
|
||||
if (!Directory.Exists(uploadPath))
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
switch (mode)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
case ImportMode.RepairRecord:
|
||||
case ImportMode.UpgradeRecord:
|
||||
{
|
||||
var exportData = new List<GenericRecordExportModel> { new GenericRecordExportModel
|
||||
{
|
||||
Date = DateTime.Now.ToShortDateString(),
|
||||
Description = "Test",
|
||||
Cost = 123.45M.ToString("C"),
|
||||
Notes = "Test Note",
|
||||
Odometer = 12345.ToString(),
|
||||
Tags = "test1 test2"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImportMode.GasRecord:
|
||||
{
|
||||
var exportData = new List<GasRecordExportModel> { new GasRecordExportModel
|
||||
{
|
||||
Date = DateTime.Now.ToShortDateString(),
|
||||
Odometer = 12345.ToString(),
|
||||
FuelConsumed = 12.34M.ToString(),
|
||||
Cost = 45.67M.ToString("C"),
|
||||
IsFillToFull = true.ToString(),
|
||||
MissedFuelUp = false.ToString(),
|
||||
Notes = "Test Note",
|
||||
Tags = "test1 test2"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteGasRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImportMode.OdometerRecord:
|
||||
{
|
||||
var exportData = new List<OdometerRecordExportModel> { new OdometerRecordExportModel
|
||||
{
|
||||
Date = DateTime.Now.ToShortDateString(),
|
||||
InitialOdometer = 12345.ToString(),
|
||||
Odometer = 12345.ToString(),
|
||||
Notes = "Test Note",
|
||||
Tags = "test1 test2"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImportMode.TaxRecord:
|
||||
{
|
||||
var exportData = new List<TaxRecordExportModel> { new TaxRecordExportModel
|
||||
{
|
||||
Date = DateTime.Now.ToShortDateString(),
|
||||
Description = "Test",
|
||||
Cost = 123.45M.ToString("C"),
|
||||
Notes = "Test Note",
|
||||
Tags = "test1 test2"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteTaxRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImportMode.SupplyRecord:
|
||||
{
|
||||
var exportData = new List<SupplyRecordExportModel> { new SupplyRecordExportModel
|
||||
{
|
||||
Date = DateTime.Now.ToShortDateString(),
|
||||
PartNumber = "TEST-123456",
|
||||
PartSupplier = "Test Supplier",
|
||||
PartQuantity = 1.5M.ToString(),
|
||||
Description = "Test",
|
||||
Cost = 123.45M.ToString("C"),
|
||||
Notes = "Test Note",
|
||||
Tags = "test1 test2"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImportMode.PlanRecord:
|
||||
{
|
||||
var exportData = new List<PlanRecordExportModel> { new PlanRecordExportModel
|
||||
{
|
||||
DateCreated = DateTime.Now.ToString(),
|
||||
DateModified = DateTime.Now.ToString(),
|
||||
Description = "Test",
|
||||
Type = ImportMode.RepairRecord.ToString(),
|
||||
Priority = PlanPriority.Normal.ToString(),
|
||||
Progress = PlanProgress.Testing.ToString(),
|
||||
Cost = 123.45M.ToString("C"),
|
||||
Notes = "Test Note"
|
||||
} };
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WritePlanRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return Json(OperationResponse.Failed("No parameters"));
|
||||
}
|
||||
try
|
||||
{
|
||||
var fileBytes = _fileHelper.GetFileBytes(fullExportFilePath, true);
|
||||
if (fileBytes.Length > 0)
|
||||
{
|
||||
return File(fileBytes, "text/csv", $"{mode.ToString().ToLower()}sample.csv");
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(OperationResponse.Failed("An error has occurred while generating CSV sample: file has zero bytes"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return Json(OperationResponse.Failed($"An error has occurred while generating CSV sample: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -473,7 +473,8 @@ namespace CarCareTracker.Controllers
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.ServiceRecord,
|
||||
ExtraFields = x.ExtraFields
|
||||
ExtraFields = x.ExtraFields,
|
||||
RequisitionHistory = x.RequisitionHistory
|
||||
}));
|
||||
//repair records
|
||||
reportData.AddRange(vehicleRecords.CollisionRecords.Select(x => new GenericReportModel
|
||||
@@ -484,7 +485,8 @@ namespace CarCareTracker.Controllers
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.RepairRecord,
|
||||
ExtraFields = x.ExtraFields
|
||||
ExtraFields = x.ExtraFields,
|
||||
RequisitionHistory = x.RequisitionHistory
|
||||
}));
|
||||
reportData.AddRange(vehicleRecords.UpgradeRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
@@ -494,7 +496,8 @@ namespace CarCareTracker.Controllers
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.UpgradeRecord,
|
||||
ExtraFields = x.ExtraFields
|
||||
ExtraFields = x.ExtraFields,
|
||||
RequisitionHistory = x.RequisitionHistory
|
||||
}));
|
||||
reportData.AddRange(vehicleRecords.TaxRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
|
||||
@@ -1129,6 +1129,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var existingPreference = existingUserColumnPreference.Single();
|
||||
existingPreference.VisibleColumns = columnPreference.VisibleColumns;
|
||||
existingPreference.ColumnOrder = columnPreference.ColumnOrder;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace CarCareTracker.Helper
|
||||
bool GetServerEnableShopSupplies();
|
||||
string GetServerPostgresConnection();
|
||||
string GetAllowedFileUploadExtensions();
|
||||
string GetServerDomain();
|
||||
bool DeleteUserConfig(int userId);
|
||||
bool GetInvariantApi();
|
||||
bool GetServerOpenRegistration();
|
||||
@@ -62,6 +63,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"));
|
||||
@@ -238,6 +244,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,
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace CarCareTracker.Helper
|
||||
public interface IFileHelper
|
||||
{
|
||||
string GetFullFilePath(string currentFilePath, bool mustExist = true);
|
||||
byte[] GetFileBytes(string fullFilePath, bool deleteFile = false);
|
||||
string MoveFileFromTemp(string currentFilePath, string newFolder);
|
||||
bool RenameFile(string currentFilePath, string newName);
|
||||
bool DeleteFile(string currentFilePath);
|
||||
@@ -85,6 +86,19 @@ namespace CarCareTracker.Helper
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
public byte[] GetFileBytes(string fullFilePath, bool deleteFile = false)
|
||||
{
|
||||
if (File.Exists(fullFilePath))
|
||||
{
|
||||
var fileBytes = File.ReadAllBytes(fullFilePath);
|
||||
if (deleteFile)
|
||||
{
|
||||
File.Delete(fullFilePath);
|
||||
}
|
||||
return fileBytes;
|
||||
}
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
public bool RestoreBackup(string fileName, bool clearExisting = false)
|
||||
{
|
||||
var fullFilePath = GetFullFilePath(fileName);
|
||||
|
||||
@@ -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.4";
|
||||
public const string VersionNumber = "1.4.7";
|
||||
public const string DbName = "data/cartracker.db";
|
||||
public const string UserConfigPath = "data/config/userConfig.json";
|
||||
public const string LegacyUserConfigPath = "config/userConfig.json";
|
||||
@@ -262,7 +262,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);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ namespace CarCareTracker.Middleware
|
||||
var userIdentity = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, splitString[0]),
|
||||
new(ClaimTypes.NameIdentifier, userData.Id.ToString())
|
||||
new(ClaimTypes.NameIdentifier, userData.Id.ToString()),
|
||||
new(ClaimTypes.Email, userData.EmailAddress),
|
||||
new(ClaimTypes.Role, "APIAuth")
|
||||
};
|
||||
if (userData.IsAdmin)
|
||||
{
|
||||
|
||||
@@ -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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
public decimal Cost { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
public bool FilterByDateRange { get; set; } = false;
|
||||
public string StartDate { get; set; } = "";
|
||||
public string EndDate { get; set; } = "";
|
||||
public bool PrintIndividualRecords { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
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,17 +125,33 @@ namespace CarCareTracker.Models
|
||||
public string DueDate { get; set; }
|
||||
[JsonConverter(typeof(FromIntOptional))]
|
||||
public string DueOdometer { get; set; }
|
||||
public string Tags { get; set; }
|
||||
}
|
||||
public class PlanRecordExportModel
|
||||
{
|
||||
[JsonConverter(typeof(FromIntOptional))]
|
||||
public string Id { get; set; }
|
||||
[JsonConverter(typeof(FromDateOptional))]
|
||||
public string DateCreated { get; set; }
|
||||
[JsonConverter(typeof(FromDateOptional))]
|
||||
public string DateModified { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Priority { get; set; }
|
||||
public string Progress { get; set; }
|
||||
[JsonConverter(typeof(FromDecimalOptional))]
|
||||
public string Cost { get; set; }
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
public class UserExportModel
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string EmailAddress { get; set; }
|
||||
[JsonConverter(typeof(FromBoolOptional))]
|
||||
public string IsAdmin { get; set; }
|
||||
[JsonConverter(typeof(FromBoolOptional))]
|
||||
public string IsRoot { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
{
|
||||
public ImportMode Tab { get; set; }
|
||||
public List<string> VisibleColumns { get; set; } = new List<string>();
|
||||
public List<string> ColumnOrder { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
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 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>
|
||||
|
||||
@@ -26,6 +26,20 @@
|
||||
<h6>Parameters</h6>
|
||||
</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/whoami</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns information for current user
|
||||
</div>
|
||||
<div class="col-3">
|
||||
No Params
|
||||
</div>
|
||||
</div>
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
<span class="badge bg-success">GET</span>
|
||||
@@ -159,6 +173,83 @@
|
||||
Id - Id of Odometer Record
|
||||
</div>
|
||||
</div>
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
<span class="badge bg-success">GET</span>
|
||||
</div>
|
||||
<div class="col-5 copyable">
|
||||
<code>/api/vehicle/planrecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of plan records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
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/planrecords/add</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Adds Plan Record to the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
<br />
|
||||
Body(form-data): {<br />
|
||||
description - Description<br />
|
||||
cost - Cost<br />
|
||||
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
|
||||
priority - Low/Normal/Critical<br />
|
||||
progress - Backlog/InProgress/Testing<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><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/planrecords/update</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Updates Plan Record
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Body(form-data): {<br />
|
||||
Id - Id of Plan Record<br />
|
||||
description - Description<br />
|
||||
cost - Cost<br />
|
||||
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
|
||||
priority - Low/Normal/Critical<br />
|
||||
progress - Backlog/InProgress/Testing<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><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/planrecords/delete</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Deletes Plan Record
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Id - Id of Plan Record
|
||||
</div>
|
||||
</div>
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
<span class="badge bg-success">GET</span>
|
||||
@@ -408,7 +499,7 @@
|
||||
<div class="col-1">
|
||||
<span class="badge bg-success">GET</span>
|
||||
</div>
|
||||
<div class="col-5 copyable">
|
||||
<div class="col-5 copyable testable">
|
||||
<code>/api/vehicle/taxrecords/check</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
@@ -578,6 +669,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>
|
||||
|
||||
@@ -27,13 +27,15 @@
|
||||
<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>
|
||||
@if (User.IsInRole("CookieAuth"))
|
||||
@if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth"))
|
||||
{
|
||||
@if (User.IsInRole(nameof(UserData.IsAdmin)))
|
||||
{
|
||||
@@ -78,13 +80,15 @@
|
||||
<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>
|
||||
@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"))
|
||||
@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>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@model KioskViewModel
|
||||
@section Scripts {
|
||||
<script src="~/lib/masonry/masonry.min.js"></script>
|
||||
<script src="~/lib/drawdown/drawdown.js"></script>
|
||||
}
|
||||
<div class="progress" role="progressbar" aria-label="Refresh Progress" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="height: 1px">
|
||||
<div class="progress-bar" style="width: 0%"></div>
|
||||
@@ -123,9 +124,15 @@
|
||||
}
|
||||
function toggleReminderNote(sender){
|
||||
var reminderNote = $(sender).find('.reminder-note');
|
||||
if (reminderNote.text().trim() != ''){
|
||||
var reminderNoteText = reminderNote.text().trim();
|
||||
if (reminderNoteText != ''){
|
||||
if (reminderNote.hasClass('d-none')) {
|
||||
reminderNote.removeClass('d-none');
|
||||
if (!reminderNote.hasClass('reminder-note-markdown')){
|
||||
let markedDownReminderNote = markdown(reminderNoteText);
|
||||
reminderNote.html(markedDownReminderNote);
|
||||
reminderNote.addClass('reminder-note-markdown');
|
||||
}
|
||||
} else {
|
||||
reminderNote.addClass('d-none');
|
||||
}
|
||||
|
||||
@@ -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,10 @@
|
||||
<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>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
@@ -253,7 +257,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 +366,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">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUserName" class="form-control">
|
||||
<input type="text" onkeyup="callBackOnEnter(event, requestPasswordReset)" id="inputUserName" class="form-control">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Request")</button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="form-group">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
|
||||
<input type="password" id="inputUserPassword" onkeyup="callBackOnEnter(event, performLogin)" class="form-control">
|
||||
<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>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUserName" class="form-control" value="@Model">
|
||||
<input type="text" id="inputUserName" class="form-control" value="@Model" onkeyup="callBackOnEnter(event, performOpenIdRegistration)">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="performOpenIdRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
|
||||
|
||||
@@ -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>
|
||||
@@ -39,7 +40,7 @@
|
||||
<div class="form-group">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<input type="password" id="inputUserPassword" class="form-control" onkeyup="callBackOnEnter(event, performRegistration)">
|
||||
<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>
|
||||
|
||||
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,16 +17,16 @@
|
||||
<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>
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<input type="password" id="inputUserPassword" class="form-control" onkeyup="callBackOnEnter(event, performPasswordReset)">
|
||||
<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>
|
||||
|
||||
@@ -21,30 +21,7 @@
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@translator.Translate(userLanguage, "Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.")
|
||||
</div>
|
||||
@if (Model == ImportMode.GasRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
else if (Model == ImportMode.TaxRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
else if (Model == ImportMode.SupplyRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
else if (Model == ImportMode.PlanRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
else if (Model == ImportMode.OdometerRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
}
|
||||
<a class="btn btn-link" href="@($"/Vehicle/GenerateCsvSample?mode={Model.ToString()}")" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -54,37 +54,37 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Odometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Description" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -93,7 +93,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -104,55 +104,55 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='daterefueled' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_DateRefueled" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_DateRefueled">@translator.Translate(userLanguage, "Date Refueled")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Odometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='delta' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Delta" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Delta">@translator.Translate(userLanguage, "Delta")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='consumption' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Consumption" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Consumption">@translator.Translate(userLanguage, "Consumption")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='fueleconomy' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_FuelEconomy" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_FuelEconomy">@translator.Translate(userLanguage, "Fuel Economy")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='unitcost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_UnitCost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_UnitCost">@translator.Translate(userLanguage, "Unit Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Notes">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -161,7 +161,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -54,37 +54,37 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='initialodometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_InitialOdometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_InitialOdometer">@translator.Translate(userLanguage, "Initial Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Odometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='distance' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Distance" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Distance">@translator.Translate(userLanguage, "Distance")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -93,7 +93,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-2 mb-2">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item text-start">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="printIndividualRecordsCheck">
|
||||
<label class="form-check-label" for="printIndividualRecordsCheck">@translator.Translate(userLanguage, "Print Individual Records")</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-2 mb-2">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item text-center" style="cursor:pointer;" onclick="showReportAdvancedParameters()">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -54,37 +54,37 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Odometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Description" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -93,7 +93,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -54,49 +54,49 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='partnumber' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_PartNumber" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_PartNumber">@translator.Translate(userLanguage, "Part Number")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='supplier' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Supplier" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Supplier">@translator.Translate(userLanguage, "Supplier")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Description" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='quantity' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Quantity" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Quantity">@translator.Translate(userLanguage, "Quantity")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -105,7 +105,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -54,31 +54,31 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Description" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -87,7 +87,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -54,37 +54,37 @@
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Date" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Odometer" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Description" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Cost" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Attachment">
|
||||
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Notes" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
@@ -93,7 +93,7 @@
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<li class="dropdown-item">
|
||||
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="@elementId">
|
||||
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</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>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
@model IEnumerable<UserColumnPreference>
|
||||
<script>
|
||||
var visibleColumns = [];
|
||||
var columnsOrder = [];
|
||||
@foreach(string visibleColumn in Model.SelectMany(x=> x.VisibleColumns))
|
||||
{
|
||||
@:visibleColumns.push(decodeHTMLEntities('@visibleColumn'));
|
||||
}
|
||||
loadUserColumnPreferences(visibleColumns);
|
||||
@foreach(string columnOrder in Model.SelectMany(x=>x.ColumnOrder)){
|
||||
@:columnsOrder.push(decodeHTMLEntities('@columnOrder'));
|
||||
}
|
||||
loadUserColumnPreferences(visibleColumns, columnsOrder);
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
var extraFields = Model.ReportParameters.ExtraFields;
|
||||
}
|
||||
<div>
|
||||
<div style="page-break-after: always;">
|
||||
<div class="row mt-2">
|
||||
<div class="d-flex">
|
||||
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
|
||||
@@ -18,13 +18,12 @@
|
||||
</span>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.StartDate) && !string.IsNullOrWhiteSpace(Model.EndDate))
|
||||
{
|
||||
<br />
|
||||
<span class="lead ms-2">
|
||||
@($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}")
|
||||
</span>
|
||||
<br />
|
||||
<span class="lead ms-2">
|
||||
@($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
@@ -93,26 +92,26 @@
|
||||
<hr />
|
||||
@if (Model.TotalDepreciation != default)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
@(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation"))
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<span><i class="bi @(Model.TotalDepreciation > 0 ? "bi-graph-down-arrow" : "bi-graph-up-arrow") me-2"></i>@Math.Abs(Model.TotalDepreciation).ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.DepreciationPerDay != default)
|
||||
{
|
||||
<div class="col-3">
|
||||
<span><i class="bi bi-calendar-event me-2"></i>@($"{Model.DepreciationPerDay.ToString("C")}/{translator.Translate(userLanguage, "day")}")</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.DepreciationPerMile != default)
|
||||
{
|
||||
<div class="col-3">
|
||||
<span><i class="bi bi-speedometer me-2"></i>@($"{Model.DepreciationPerMile.ToString("C")}/{Model.DistanceUnit}")</span>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
@(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation"))
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<span><i class="bi @(Model.TotalDepreciation > 0 ? "bi-graph-down-arrow" : "bi-graph-up-arrow") me-2"></i>@Math.Abs(Model.TotalDepreciation).ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.DepreciationPerDay != default)
|
||||
{
|
||||
<div class="col-3">
|
||||
<span><i class="bi bi-calendar-event me-2"></i>@($"{Model.DepreciationPerDay.ToString("C")}/{translator.Translate(userLanguage, "day")}")</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.DepreciationPerMile != default)
|
||||
{
|
||||
<div class="col-3">
|
||||
<span><i class="bi bi-speedometer me-2"></i>@($"{Model.DepreciationPerMile.ToString("C")}/{Model.DistanceUnit}")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr />
|
||||
}
|
||||
<div class="row">
|
||||
@@ -177,3 +176,113 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.ReportParameters.PrintIndividualRecords){
|
||||
@foreach (GenericReportModel genericRecord in Model.VehicleHistory)
|
||||
{
|
||||
<div class="d-flex flex-column recordSticker">
|
||||
<div class="d-flex">
|
||||
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span class="display-6">@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="lead">@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")</span>
|
||||
</li>
|
||||
@foreach (ExtraField extraField in Model.VehicleData.ExtraFields)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(extraField.Value))
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<span class="lead">@($"{extraField.Name}: {extraField.Value}")</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<ul class="list-group">
|
||||
@if (!string.IsNullOrWhiteSpace(genericRecord.Description))
|
||||
{
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Description")}: {genericRecord.Description}")
|
||||
</li>
|
||||
}
|
||||
@switch (genericRecord.DataType)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
case ImportMode.RepairRecord:
|
||||
case ImportMode.UpgradeRecord:
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Odometer}")
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
|
||||
</li>
|
||||
break;
|
||||
case ImportMode.TaxRecord:
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
|
||||
</li>
|
||||
break;
|
||||
}
|
||||
@foreach (ExtraField extraField in genericRecord.ExtraFields)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
@($"{extraField.Name}: {extraField.Value}")
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
@if (genericRecord.RequisitionHistory.Any())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part Number")</th>
|
||||
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Description")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Quantity")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Cost")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (SupplyUsageHistory usageHistory in genericRecord.RequisitionHistory)
|
||||
{
|
||||
<tr class="d-flex">
|
||||
<td class="col-2 text-truncate">@usageHistory.PartNumber</td>
|
||||
<td class="col-6 text-truncate">@usageHistory.Description</td>
|
||||
<td class="col-2">@usageHistory.Quantity.ToString("F")</td>
|
||||
<td class="col-2">@usageHistory.Cost.ToString("C2")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
}
|
||||
<div class="row flex-grow-1 flex-shrink-1">
|
||||
<div class="col-12">
|
||||
<div class="stickerNote ms-1 me-1 p-1">
|
||||
@(genericRecord.Notes)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<script>setMarkDownStickerNotes()</script>
|
||||
}
|
||||
@@ -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,7 @@
|
||||
"EnableAutoReminderRefresh": false,
|
||||
"EnableAutoOdometerInsert": false,
|
||||
"EnableShopSupplies": false,
|
||||
"ShowCalendar": true,
|
||||
"EnableExtraFieldColumns": false,
|
||||
"UseUKMPG": false,
|
||||
"UseThreeDecimalGasCost": true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="icon" type="image/x-icon" href="../favicon.ico">
|
||||
<title>LubeLogger Configurator</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
@@ -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">
|
||||
@@ -227,7 +237,9 @@
|
||||
<textarea id="outputModalText" readonly style="width:100%; height:450px;"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-strip me-auto" onclick="removeDoubleQuotes()">Remove Double Quotes</button>
|
||||
<button type="button" class="btn btn-secondary btn-strip me-auto" onclick="removeDoubleQuotes()">Remove Double Quotes</button>
|
||||
<input id="appSettingsUpload" onChange="readUploadedFile()" class="d-none" type="file" accept="application/json">
|
||||
<button type="button" class="btn btn-secondary btn-upload me-auto" onclick="uploadAndMerge()">Upload appsettings.json</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary btn-copy" onclick="copyToClipboard()">Copy</button>
|
||||
</div>
|
||||
@@ -237,6 +249,44 @@
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
function uploadAndMerge(){
|
||||
$("#appSettingsUpload").click();
|
||||
}
|
||||
function readUploadedFile(){
|
||||
let fl_files = $("#appSettingsUpload")[0].files; // JS FileList object
|
||||
|
||||
if (fl_files.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use the 1st file from the list
|
||||
let fl_file = fl_files[0];
|
||||
|
||||
let reader = new FileReader(); // built in API
|
||||
|
||||
let display_file = ( e ) => { // set the contents of the <textarea>
|
||||
mergeIntoUploadedFile(e.target.result);
|
||||
};
|
||||
|
||||
let on_reader_load = ( fl ) => {
|
||||
return display_file; // a function
|
||||
};
|
||||
|
||||
// Closure to capture the file information.
|
||||
reader.onload = on_reader_load( fl_file );
|
||||
|
||||
// Read the file as text.
|
||||
reader.readAsText( fl_file );
|
||||
}
|
||||
function mergeIntoUploadedFile(fileContents){
|
||||
var newJsonObject = JSON.parse("{" + $("#outputModalText").text() + "}");
|
||||
var currentJsonObject = JSON.parse(fileContents);
|
||||
var mergedJsonObject = {...currentJsonObject, ...newJsonObject};
|
||||
$("#outputModalLabel").text("Content for appsettings.json");
|
||||
$("#outputModalText").text(JSON.stringify(mergedJsonObject, null, 2));
|
||||
//clear out uploaded file content
|
||||
$("#appSettingsUpload").val("");
|
||||
}
|
||||
function removeDoubleQuotes(){
|
||||
var currentText = $("#outputModalText").text();
|
||||
$("#outputModalText").text(currentText.replaceAll('"', ''));
|
||||
@@ -276,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(),
|
||||
@@ -292,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"),
|
||||
@@ -319,6 +373,11 @@ function generateConfig(){
|
||||
$("#outputModalLabel").text("Append into appsettings.json");
|
||||
$("#outputModalText").text(JSON.stringify(windowConfig, null, 2).slice(1,-1));
|
||||
$(".btn-strip").hide();
|
||||
if (jQuery.isEmptyObject(windowConfig)){
|
||||
$(".btn-upload").hide();
|
||||
} else {
|
||||
$(".btn-upload").show();
|
||||
}
|
||||
$("#outputModal").modal("show");
|
||||
} else {
|
||||
var dockerConfig = [];
|
||||
@@ -341,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')}"`);
|
||||
}
|
||||
@@ -360,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')}`);
|
||||
@@ -375,6 +438,7 @@ function generateConfig(){
|
||||
$("#outputModalLabel").text("Content for .env");
|
||||
$("#outputModalText").text(dockerConfig.join("\r\n"));
|
||||
$(".btn-strip").show();
|
||||
$(".btn-upload").hide();
|
||||
$("#outputModal").modal("show");
|
||||
}
|
||||
}
|
||||
8
my-.env
Normal file
8
my-.env
Normal file
@@ -0,0 +1,8 @@
|
||||
LC_ALL=en_US.UTF-8
|
||||
LANG=en_US.UTF-8
|
||||
MailConfig__EmailServer="box.cahaa.net"
|
||||
MailConfig__EmailFrom="lubelog@cahaa.net"
|
||||
MailConfig__UseSSL="true"
|
||||
MailConfig__Port=465
|
||||
MailConfig__Username="clint@cahaa.net"
|
||||
MailConfig__Password="***************"
|
||||
28
my-docker-compose.yml
Normal file
28
my-docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/hargata/lubelogger:latest
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
# volumes used to keep data persistent
|
||||
volumes:
|
||||
- config:/App/config
|
||||
- data:/App/data
|
||||
- documents:/App/wwwroot/documents
|
||||
- images:/App/wwwroot/images
|
||||
- temp:/App/wwwroot/temp
|
||||
- log:/App/log
|
||||
- keys:/root/.aspnet/DataProtection-Keys
|
||||
# expose port and/or use serving via traefik
|
||||
ports:
|
||||
- 18180:8080
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
config: null
|
||||
data: null
|
||||
documents: null
|
||||
images: null
|
||||
temp: null
|
||||
log: null
|
||||
keys: null
|
||||
networks: {}
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
Date,Odometer,FuelConsumed,Cost,IsFillToFull,MissedFuelUp,Notes
|
||||
5/8/2020,204836,8.331,16.24,True,False,
|
||||
5/30/2020,205056,11.913,25.72,True,False,
|
||||
|
@@ -1,2 +0,0 @@
|
||||
Date,Odometer,Notes
|
||||
1/1/2024,260001,test test
|
||||
|
@@ -1,2 +0,0 @@
|
||||
DateCreated,DateModified,Description,Notes,Type,Priority,Progress,Cost
|
||||
1/19/2024 6:01:02 PM,1/19/2024 7:32:58 PM,Repair Exhaust,,RepairRecord,Normal,Testing,$50.00
|
||||
|
@@ -1,3 +0,0 @@
|
||||
Date,Odometer,Description,Notes,Cost
|
||||
01/01/2020,45000,Test Description 1,Test Note 1,20.60
|
||||
01/02/2020,47000,Test Description 2,Test Note 2,40.45
|
||||
|
@@ -1,2 +0,0 @@
|
||||
Date,PartNumber,PartSupplier,PartQuantity,Description,Cost,Notes
|
||||
1/16/2024,EVA17872045551,Evan Fischer,1,Front Bumper,$95.14,Altima Activities
|
||||
|
@@ -1,3 +0,0 @@
|
||||
Date,Description,Notes,Cost
|
||||
01/01/2020,Test Description 1,Test Note 1,20.60
|
||||
01/02/2020,Test Description 2,Test Note 2,40.45
|
||||
|
@@ -319,15 +319,17 @@ function updateMPGLabels() {
|
||||
var rowsToAggregate = $("[data-aggregated='true']").parent(":not('.override-hide')");
|
||||
var rowsUnaggregated = $("[data-aggregated='false']").parent(":not('.override-hide')");
|
||||
var rowMPG = rowsToAggregate.children('[data-gas-type="fueleconomy"]').toArray().map(x => globalParseFloat(x.textContent));
|
||||
var rowNonZeroMPG = rowMPG.filter(x => x > 0);
|
||||
var maxMPG = rowMPG.length > 0 ? rowMPG.reduce((a, b) => a > b ? a : b) : 0;
|
||||
var minMPG = rowMPG.length > 0 ? rowMPG.filter(x=>x>0).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) {
|
||||
if (totalGasConsumed > 0 && rowNonZeroMPG.length > 0) {
|
||||
var averageMPG = totalMilesTraveled / totalGasConsumed;
|
||||
if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l' && averageMPG > 0) {
|
||||
averageMPG = 100 / averageMPG;
|
||||
|
||||
@@ -55,12 +55,6 @@ function performPasswordReset() {
|
||||
});
|
||||
}
|
||||
|
||||
function handlePasswordKeyPress(event) {
|
||||
if (event.keyCode == 13) {
|
||||
performLogin();
|
||||
}
|
||||
}
|
||||
|
||||
function remoteLogin() {
|
||||
$.get('/Login/GetRemoteLoginLink', function (data) {
|
||||
if (data) {
|
||||
|
||||
@@ -7,6 +7,7 @@ function getAndValidateSelectedColumns() {
|
||||
var tagFilterMode = $("#tagSelector").val();
|
||||
var tagsToFilter = $("#tagSelectorInput").val();
|
||||
var filterByDateRange = $("#dateRangeSelector").is(":checked");
|
||||
var printIndividualRecords = $("#printIndividualRecordsCheck").is(":checked");
|
||||
var startDate = $("#dateRangeStartDate").val();
|
||||
var endDate = $("#dateRangeEndDate").val();
|
||||
$("#columnSelector :checked").map(function () {
|
||||
@@ -42,7 +43,8 @@ function getAndValidateSelectedColumns() {
|
||||
tags: [],
|
||||
filterByDateRange: filterByDateRange,
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
endDate: '',
|
||||
printIndividualRecords: printIndividualRecords
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
@@ -54,7 +56,8 @@ function getAndValidateSelectedColumns() {
|
||||
tags: tagsToFilter,
|
||||
filterByDateRange: filterByDateRange,
|
||||
startDate: startDate,
|
||||
endDate: endDate
|
||||
endDate: endDate,
|
||||
printIndividualRecords: printIndividualRecords
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +83,7 @@ function getSavedReportParameters() {
|
||||
$("#dateRangeSelector").prop('checked', selectedReportColumns.filterByDateRange);
|
||||
$("#dateRangeStartDate").val(selectedReportColumns.startDate);
|
||||
$("#dateRangeEndDate").val(selectedReportColumns.endDate);
|
||||
$("#printIndividualRecordsCheck").prop('checked', selectedReportColumns.printIndividualRecords);
|
||||
}
|
||||
}
|
||||
function generateVehicleHistoryReport() {
|
||||
|
||||
@@ -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,7 @@ function updateSettings() {
|
||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
||||
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
|
||||
showCalendar: $("#showCalendar").is(":checked"),
|
||||
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
|
||||
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
|
||||
preferredGasUnit: $("#preferredGasUnit").val(),
|
||||
@@ -85,6 +95,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;
|
||||
|
||||
@@ -336,6 +336,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 +712,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() == '') {
|
||||
@@ -1370,7 +1380,7 @@ function searchTableRows(tabName) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function loadUserColumnPreferences(columns) {
|
||||
function loadUserColumnPreferences(columns, order) {
|
||||
if (columns.length == 0) {
|
||||
//user has no preference saved, reset to default
|
||||
return;
|
||||
@@ -1387,12 +1397,35 @@ function loadUserColumnPreferences(columns) {
|
||||
$(`[data-column='${x}']`).show();
|
||||
}
|
||||
});
|
||||
order.map((x, y) => {
|
||||
//re-order items in menu
|
||||
var itemToMove = $(`[data-column-toggle='${x}'].col-visible-toggle`).closest('.dropdown-item');
|
||||
var itemCurrentlyInPosition = $('.dropdown-item[draggable="true"]')[y];
|
||||
if (itemToMove != undefined && itemToMove.length > 0 && itemCurrentlyInPosition != undefined) {
|
||||
itemToMove.insertBefore(itemCurrentlyInPosition);
|
||||
}
|
||||
//re-order table columns
|
||||
$(`[data-column='${x}']`).css('order', y);
|
||||
});
|
||||
}
|
||||
function saveUserColumnPreferences(importMode) {
|
||||
var visibleColumns = $('.col-visible-toggle:checked').map((index, elem) => $(elem).attr('data-column-toggle')).toArray();
|
||||
var columnOrder = [];
|
||||
var sortedOrderedColumns = $("ul.dropdown-menu > li[draggable='true']").toArray().sort((a, b) => {
|
||||
var currentVal = $(a).css("order");
|
||||
var nextVal = $(b).css("order");
|
||||
return currentVal - nextVal;
|
||||
});
|
||||
sortedOrderedColumns.map(elem => {
|
||||
var columnOrderName = $(elem).find('.col-visible-toggle').attr("data-column-toggle");
|
||||
if (columnOrderName != null && columnOrderName != undefined) {
|
||||
columnOrder.push(columnOrderName);
|
||||
}
|
||||
});
|
||||
var columnPreference = {
|
||||
tab: importMode,
|
||||
visibleColumns: visibleColumns
|
||||
visibleColumns: visibleColumns,
|
||||
columnOrder: columnOrder
|
||||
};
|
||||
$.post('/Vehicle/SaveUserColumnPreferences', { columnPreference: columnPreference }, function (data) {
|
||||
if (!data) {
|
||||
@@ -1472,4 +1505,75 @@ function togglePasswordVisibility(elem) {
|
||||
passwordButton.removeClass('bi-eye-slash');
|
||||
passwordButton.addClass('bi-eye');
|
||||
}
|
||||
}
|
||||
var tableColumnDragToReorder = undefined;
|
||||
function handleTableColumnDragStart(e) {
|
||||
tableColumnDragToReorder = $(e.target).closest('.dropdown-item');
|
||||
//clear out order attribute.
|
||||
$("ul.dropdown-menu > li[draggable='true']").map((index, elem) => {
|
||||
$(elem).css('order', 0);
|
||||
})
|
||||
}
|
||||
function handleTableColumnDragOver(e) {
|
||||
if (tableColumnDragToReorder == undefined || tableColumnDragToReorder == "") {
|
||||
return;
|
||||
}
|
||||
var potentialDropTarget = $(e.target).closest('.list-group-item').find('.col-visible-toggle').attr("data-column-toggle");
|
||||
var draggedTarget = tableColumnDragToReorder.find('.col-visible-toggle').attr("data-column-toggle");
|
||||
if (draggedTarget != potentialDropTarget) {
|
||||
var targetObj = $(e.target).closest('.dropdown-item');
|
||||
var draggedOrder = tableColumnDragToReorder.index();
|
||||
var targetOrder = targetObj.index();
|
||||
if (draggedOrder < targetOrder) {
|
||||
tableColumnDragToReorder.insertAfter(targetObj);
|
||||
} else {
|
||||
tableColumnDragToReorder.insertBefore(targetObj);
|
||||
}
|
||||
}
|
||||
else {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
function handleTableColumnDragEnd(tabName) {
|
||||
$("ul.dropdown-menu > li[draggable='true']").map((index, elem) => {
|
||||
$(elem).css('order', $(elem).index());
|
||||
var columnName = $(elem).find('.col-visible-toggle').attr('data-column-toggle');
|
||||
$(`[data-column='${columnName}']`).css('order', $(elem).index());
|
||||
});
|
||||
saveUserColumnPreferences(tabName);
|
||||
tableColumnDragToReorder = undefined;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,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 +356,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 +590,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 +603,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 +620,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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -98,8 +98,8 @@
|
||||
replace(rx_link, function(all, p1, p2, p3, p4, p5, p6) {
|
||||
stash[--si] = p4
|
||||
? p2
|
||||
? '<img src="' + p4 + '" alt="' + p3 + '"/>'
|
||||
: '<a href="' + p4 + '">' + unesc(highlight(p3)) + '</a>'
|
||||
? '<img style="max-width:100%;max-height:100%;object-fit:scale-down;" src="' + p4 + '" alt="' + p3 + '"/>'
|
||||
: '<a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" target="_blank" href="' + p4 + '">' + unesc(highlight(p3)) + '</a>'
|
||||
: p6;
|
||||
return si + '\uf8ff';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user