Compare commits

...

103 Commits

Author SHA1 Message Date
DESKTOP-T0O5CDB\DESK-555BD
29680cf0e9 made vehicle title the edit button. 2025-05-17 10:39:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
723eb1a769 Added API endpoint to check for latest release. 2025-05-17 10:14:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
732a628c20 Merge branch 'Hargata/148.changes' into Hargata/updated.layout 2025-05-17 09:49:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84d40edb0e set graph grace see #941 2025-05-16 19:58:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9842f0e501 add report header. 2025-05-16 19:23:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
44c5f921a5 Merge branch 'Hargata/908' into Hargata/updated.layout 2025-05-16 16:54:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fae4aa31aa show vehicle thumbnail 2025-05-16 14:45:06 -06:00
Hargata Softworks
adb505c87c Merge pull request #950 from iamdabe/vehicle-home-thumbnail
Setting to show the vehicle thumbnail in the header
2025-05-16 14:31:45 -06:00
Hargata Softworks
d21b0a9e29 Merge branch 'Hargata/updated.layout' into vehicle-home-thumbnail 2025-05-16 14:31:30 -06:00
Hargata Softworks
15e4181aff Merge pull request #949 from iamdabe/main
Updated _NoteModal.cshtml label to correctly target file input 'noteFiles'
2025-05-16 14:25:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a4627ef02 use regular sized logo in mobile view in garage 2025-05-16 14:12:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d9e11273bd fix admin panel view 2025-05-16 13:49:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8e6f40a1b1 Updated layout for post hiatus release. 2025-05-16 13:44:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
55f86ecb3f add reportheader. 2025-05-16 11:11:10 -06:00
Dave Walker
8eef1465cf First commit for new setting to show the vehicle thumbnail in the header of each vehicle page. 2025-05-15 18:22:48 +02:00
iamdabe
fd0ffba7c4 Fixed _NoteModal.cshtml label to correctly target file input 'noteFiles' 2025-05-15 17:33:43 +02:00
DESKTOP-T0O5CDB\DESK-555BD
afb710e6ad Add server API version so that we can track the current version. 2025-04-30 09:51:04 -06:00
Hargata Softworks
419b755e7a Merge pull request #937 from hargata/Hargata/update.configurator.2
Add Server Domain into configurator
2025-04-27 08:58:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8cd5342c02 Add Server Domain into configurator 2025-04-27 08:57:48 -06:00
Hargata Softworks
fa32ecdb5a Merge pull request #916 from hargata/Hargata/oidc.userinfo
V1.4.7 Changes
2025-04-27 08:41:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d08726bd85 Add LUBELOGGER_DOMAIN environment variable which is used to craft registration and reset password links to reduce user friction. 2025-04-27 08:37:08 -06:00
Hargata Softworks
a87861069b Merge pull request #936 from hargata/Hargata/877
Hargata/877 - API Endpoints for Basic Reminders
2025-04-26 09:35:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b6d6a8765d update API documentation 2025-04-26 09:32:15 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f307c0933a add delete endpoint 2025-04-26 09:16:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9968ccb541 add put API 2025-04-26 09:11:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5d3746f168 add POST api endpoint for basic reminders. 2025-04-26 08:56:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
df0c4eeca2 Merge branch 'Hargata/oidc.userinfo' into Hargata/877 2025-04-26 08:15:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5b3c0aed72 Add Extra Field Type to multi edit modals 2025-04-26 07:53:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
040728b96d Fix 931 2025-04-24 20:54:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8b7b3e4ba optimized styling for planners tab 2025-04-16 11:16:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cf1f1a884b add tooltip for real long file names 2025-04-16 10:01:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c470db0590 minor UI fix on garage add. 2025-04-09 08:55:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cb71650adf API hardening, allow nulls for list types in API payload, will auto convert to empty list. 2025-04-08 08:15:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
923d59af0a update configurator 2025-04-05 08:26:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d68e1a3939 Add userinfo to retrieve email claim if not provided in id_token 2025-04-05 08:17:40 -06:00
Hargata Softworks
bf954a2946 Merge pull request #905 from hargata/Hargata/oidc.check.claims
Add debug page for advance OIDC debugging
2025-03-31 07:49:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d3f29be227 Add debug page for advance OIDC debugging 2025-03-31 07:48:29 -06:00
Hargata Softworks
5afe88a33a Merge pull request #904 from hargata/Hargata/oidc.check.claims
add openid troubleshooting mode.
2025-03-30 20:40:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6937eff576 add troubleshooting mode. 2025-03-30 20:39:25 -06:00
Hargata Softworks
6371cccc48 Merge pull request #903 from hargata/Hargata/oidc.check.claims
set default oidc claims to openid email
2025-03-30 11:30:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fc174160e0 set default oidc claims to openid email 2025-03-30 11:28:29 -06:00
Hargata Softworks
876f99fd26 Merge pull request #901 from hargata/Hargata/oidc.check.claims
Add check for email claims.
2025-03-29 07:40:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
75c65b4681 Add check for email claims. 2025-03-29 07:37:09 -06:00
Hargata Softworks
76031d27d7 Merge pull request #900 from hargata/Hargata/612
Add extra field type.
2025-03-28 10:00:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4079a93c3e Add extra field type. 2025-03-28 09:40:13 -06:00
Hargata Softworks
d913ab2009 Merge pull request #899 from hargata/Hargata/update.deps
Hargata/update.deps
2025-03-27 06:58:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6bfbcc4374 More dependencies 2025-03-27 06:57:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
73d9a7e6e9 CHORE: Update Dependencies 2025-03-27 06:50:02 -06:00
Hargata Softworks
3cbce8b584 Merge pull request #898 from hargata/Hargata/879
Allow users to hide calendar tab.
2025-03-27 06:36:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e4fcb52b24 Allow users to hide calendar tab. 2025-03-27 06:34:57 -06:00
Hargata Softworks
ae4f01ac9a Merge pull request #897 from hargata/Hargata/893
Allow users to hide Done and Delete columns.
2025-03-27 06:10:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
46b3845b3e Allow users to hide Done and Delete columns. 2025-03-27 06:06:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ffb126276f Add ID to reminderexportmodel. 2025-03-27 05:59:01 -06:00
Hargata Softworks
a9e4be823d Merge pull request #896 from hargata/Hargata/895
Hargata/895
2025-03-27 05:56:03 -06:00
DESKTOP-T0O5CDB\DESK-555BD
56cae008a6 Fixed UI and odometer adjustments invariant formats. 2025-03-27 05:54:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c2dd379ea3 Fixed vehicle API not respecting locale invariant format. 2025-03-27 05:45:48 -06:00
Hargata Softworks
baa569b323 Merge pull request #894 from hargata/Hargata/893
added urgency and due metrics for recurring reminder selector
2025-03-26 11:51:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
64ce0f8c07 added urgency and due metrics for recurring reminder selector 2025-03-26 11:33:00 -06:00
Hargata Softworks
bd98e5a6cd Merge pull request #885 from hargata/Hargata/146.changes
Display current odometer when incrementing.
2025-03-22 18:49:01 -06:00
Hargata Softworks
83fc8b8682 Merge pull request #891 from hargata/Hargata/889
remove unused aggregation.
2025-03-22 18:20:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
89fa15bbb7 remove unused aggregation. 2025-03-22 18:20:29 -06:00
Hargata Softworks
5e69be56aa Merge pull request #890 from hargata/Hargata/889
Fixed Average MPG label changing when consumption units are changed.
2025-03-22 18:11:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b1112dc617 Fixed Average MPG label changing when consumption units are changed. 2025-03-22 18:04:41 -06:00
Hargata Softworks
5a31460afe Merge pull request #886 from hargata/Hargata/server.config
Review server configs
2025-03-20 11:26:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2bcedbc7d4 translation keys 2025-03-20 11:25:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5047fdf1bc fixed button styling. 2025-03-20 11:23:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7c8c3fb1c8 added test email 2025-03-20 10:34:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9920bd472f added oidc config 2025-03-20 09:49:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d7839a8a05 Allow users to see server configuration passed in via environment variables or appsettings.json 2025-03-19 12:57:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7e07e73ef5 Display current odometer when incrementing. 2025-03-18 12:41:02 -06:00
Hargata Softworks
72a5960d40 Merge pull request #873 from hargata/Hargata/update.configurator
Fix Favicon Path
2025-02-27 12:04:18 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3429f1e4f9 Fix Favicon Path 2025-02-27 12:03:22 -07:00
Hargata Softworks
f8bea8bf81 Merge pull request #872 from hargata/Hargata/update.configurator
Update Configurator
2025-02-27 11:58:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
43794dd223 add feature to automatically merge new configurations into existing appsettings.json 2025-02-27 11:57:18 -07:00
Hargata Softworks
5cc84a7b46 Merge pull request #866 from hargata/Hargata/enter.key.qol
use generic enter key callback to handle key presses.
2025-02-19 16:09:17 -07:00
DESKTOP-T0O5CDB\DESK-555BD
48248a4386 use generic enter key callback to handle key presses. 2025-02-19 16:08:15 -07:00
Hargata Softworks
95305402e6 Merge pull request #864 from hargata/Hargata/856
add markdown to kiosk notes.
2025-02-19 09:12:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
29f24c527f add markdown to kiosk notes. 2025-02-19 09:10:03 -07:00
Hargata Softworks
efa2bbf6cc Merge pull request #858 from hargata/Hargata/857
Add flag to print individual records
2025-02-12 10:24:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
66ed9ba699 add flag to print individual records when generating vehicle history report. 2025-02-12 10:23:19 -07:00
Hargata Softworks
e841e64f78 Merge pull request #852 from hargata/Hargata/dynamic.csv
dynamically generate locale-sensitive CSV samples
2025-02-10 11:15:18 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b0f46803c5 dynamically generate locale-sensitive CSV samples 2025-02-10 11:13:53 -07:00
Hargata Softworks
ac0bef5de0 Merge pull request #850 from hargata/Hargata/840.2
more hardening.
2025-02-07 12:37:22 -07:00
DESKTOP-T0O5CDB\DESK-555BD
08ea40a08e more hardening. 2025-02-07 12:36:43 -07:00
Hargata Softworks
1d8e8059cd Merge pull request #849 from hargata/Hargata/848
fixed MPG label bugs when all tagged records are partial fuel ups.
2025-02-07 08:33:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
20b4396a4c simplified code. 2025-02-07 08:32:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4c39cb4d06 fixed MPG label bugs when all tagged records are partial fuel ups. 2025-02-07 08:29:18 -07:00
Hargata Softworks
f58e6abd9f Update issue templates 2025-02-06 22:48:32 -07:00
Hargata Softworks
4f1e83a7d7 Update issue templates 2025-02-06 22:46:55 -07:00
Hargata Softworks
e39b38c2d5 Update CONTRIBUTING.md 2025-02-06 22:35:38 -07:00
Hargata Softworks
b9ffa6ce91 Merge pull request #845 from hargata/Hargata/780
further hardening.
2025-02-05 16:28:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
76c6753785 further hardening. 2025-02-05 16:27:45 -07:00
Hargata Softworks
54f5062377 Merge pull request #844 from hargata/Hargata/780
add functionality to re-arrange table columns.
2025-02-05 16:01:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7e60fd9e40 reset var. 2025-02-05 16:00:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
926947bae4 add functionality to re-arrange table columns. 2025-02-05 15:56:20 -07:00
Hargata Softworks
2652e47018 Merge pull request #841 from hargata/Hargata/840
Hargata/840
2025-02-05 07:06:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c1500b6ed0 bump version 2025-02-04 13:22:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
125bc44d2e add put and delete api endpoints. 2025-02-04 13:15:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c317c0a058 Add POST API Endpoint to add plan records. 2025-02-04 10:07:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
beb9498399 standardize this stuff. 2025-02-03 16:50:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4e74940684 added whoami endpoint 2025-02-03 16:02:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c0f73080d2 Add APIAuth Role 2025-02-03 13:34:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cd0a35537b API endpoint for Planner 2025-02-03 13:20:08 -07:00
102 changed files with 2563 additions and 500 deletions

View File

@@ -8,6 +8,8 @@ Ideally, the Issues tab should only consist of bug reports and feature requests
## Feature Requests ## Feature Requests
Feature Requests are cool, but we do want to avoid bloat and scope/feature creep. 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. LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker.
It is not and should not be used for the following: It is not and should not be used for the following:
- Project Management Software(e.g.: Jira) - Project Management Software(e.g.: Jira)

View File

@@ -1,7 +1,7 @@
--- ---
name: Bug report name: Bug report
about: Report a bug about: Report a bug
title: '' title: "[BUG]"
labels: '' labels: ''
assignees: '' assignees: ''
@@ -18,7 +18,7 @@ Please make sure you have performed the following steps before opening a new bug
**Platform** **Platform**
- [ ] Docker Image - [ ] Docker Image
- [ ] Windows Standalone Executable - [ ] Windows/Linux Standalone Executable
**Browser Console Errors(F12)** **Browser Console Errors(F12)**
<!-- Attach a screenshot or codeblock containing the browser console error --> <!-- Attach a screenshot or codeblock containing the browser console error -->

View 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 -->

View File

@@ -11,10 +11,10 @@
</ItemGroup> </ItemGroup>
<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="LiteDB" Version="5.0.17" />
<PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Npgsql" Version="8.0.5" /> <PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup> </ItemGroup>

View File

@@ -92,6 +92,54 @@ namespace CarCareTracker.Controllers
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
} }
[HttpGet] [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/version")]
public async Task<IActionResult> ServerVersion(bool checkForUpdate = false)
{
var viewModel = new ReleaseVersion
{
CurrentVersion = StaticHelper.VersionNumber,
LatestVersion = StaticHelper.VersionNumber
};
if (checkForUpdate)
{
try
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request");
var releaseResponse = await httpClient.GetFromJsonAsync<ReleaseResponse>(StaticHelper.ReleasePath) ?? new ReleaseResponse();
if (!string.IsNullOrWhiteSpace(releaseResponse.tag_name))
{
viewModel.LatestVersion = releaseResponse.tag_name;
}
}
catch (Exception ex)
{
return Json(OperationResponse.Failed($"Unable to retrieve latest version from GitHub API: {ex.Message}"));
}
}
return Json(viewModel);
}
[HttpGet]
[Route("/api/vehicles")] [Route("/api/vehicles")]
public IActionResult Vehicles() public IActionResult Vehicles()
{ {
@@ -100,7 +148,14 @@ namespace CarCareTracker.Controllers
{ {
result = _userLogic.FilterUserVehicles(result, GetUserID()); 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] [HttpGet]
@@ -153,6 +208,227 @@ namespace CarCareTracker.Controllers
return Json(convertedOdometer); 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 #region ServiceRecord
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
@@ -198,6 +474,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
var serviceRecord = new ServiceRecord() var serviceRecord = new ServiceRecord()
@@ -278,6 +562,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -360,6 +652,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
var repairRecord = new CollisionRecord() var repairRecord = new CollisionRecord()
@@ -441,6 +741,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -524,6 +832,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
var upgradeRecord = new UpgradeRecord() var upgradeRecord = new UpgradeRecord()
@@ -604,6 +920,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -720,6 +1044,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty.")); 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 try
{ {
var taxRecord = new TaxRecord() var taxRecord = new TaxRecord()
@@ -783,6 +1115,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -882,6 +1222,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty.")); 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 try
{ {
var odometerRecord = new OdometerRecord() var odometerRecord = new OdometerRecord()
@@ -943,6 +1291,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -1042,6 +1398,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); 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 try
{ {
var gasRecord = new GasRecord() var gasRecord = new GasRecord()
@@ -1121,6 +1485,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); 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 try
{ {
//retrieve existing record //retrieve existing record
@@ -1160,6 +1532,7 @@ namespace CarCareTracker.Controllers
} }
} }
#endregion #endregion
#region ReminderRecord
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
[Route("/api/vehicle/reminders")] [Route("/api/vehicle/reminders")]
@@ -1172,7 +1545,7 @@ namespace CarCareTracker.Controllers
} }
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId); var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(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")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(results, StaticHelper.GetInvariantOption()); return Json(results, StaticHelper.GetInvariantOption());
@@ -1182,6 +1555,183 @@ namespace CarCareTracker.Controllers
return Json(results); 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] [HttpGet]
[Route("/api/calendar")] [Route("/api/calendar")]
public IActionResult Calendar() public IActionResult Calendar()
@@ -1195,6 +1745,7 @@ namespace CarCareTracker.Controllers
var calendarContent = StaticHelper.RemindersToCalendar(reminders); var calendarContent = StaticHelper.RemindersToCalendar(reminders);
return File(calendarContent, "text/calendar"); return File(calendarContent, "text/calendar");
} }
#endregion
[HttpPost] [HttpPost]
[Route("/api/documents/upload")] [Route("/api/documents/upload")]
public IActionResult UploadDocument(List<IFormFile> documents) public IActionResult UploadDocument(List<IFormFile> documents)

View File

@@ -6,7 +6,7 @@ namespace CarCareTracker.Controllers
{ {
public IActionResult Unauthorized() public IActionResult Unauthorized()
{ {
if (!User.IsInRole("CookieAuth")) if (User.IsInRole("APIAuth"))
{ {
Response.StatusCode = 403; Response.StatusCode = 403;
return new EmptyResult(); return new EmptyResult();

View File

@@ -23,6 +23,7 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess; private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper; private readonly IReminderHelper _reminderHelper;
private readonly ITranslationHelper _translationHelper; private readonly ITranslationHelper _translationHelper;
private readonly IMailHelper _mailHelper;
public HomeController(ILogger<HomeController> logger, public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess, IVehicleDataAccess dataAccess,
IUserLogic userLogic, IUserLogic userLogic,
@@ -33,7 +34,8 @@ namespace CarCareTracker.Controllers
IExtraFieldDataAccess extraFieldDataAccess, IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess, IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper, IReminderHelper reminderHelper,
ITranslationHelper translationHelper) ITranslationHelper translationHelper,
IMailHelper mailHelper)
{ {
_logger = logger; _logger = logger;
_dataAccess = dataAccess; _dataAccess = dataAccess;
@@ -46,6 +48,7 @@ namespace CarCareTracker.Controllers
_loginLogic = loginLogic; _loginLogic = loginLogic;
_vehicleLogic = vehicleLogic; _vehicleLogic = vehicleLogic;
_translationHelper = translationHelper; _translationHelper = translationHelper;
_mailHelper = mailHelper;
} }
private int GetUserID() private int GetUserID()
{ {
@@ -555,6 +558,29 @@ namespace CarCareTracker.Controllers
} }
return Json(false); 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)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() public IActionResult Error()
{ {

View File

@@ -49,21 +49,31 @@ namespace CarCareTracker.Controllers
} }
return View(model: redirectURL); return View(model: redirectURL);
} }
public IActionResult Registration() public IActionResult Registration(string token = "", string email = "")
{ {
if (_config.GetServerDisabledRegistration()) if (_config.GetServerDisabledRegistration())
{ {
return RedirectToAction("Index"); 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() public IActionResult ForgotPassword()
{ {
return View(); 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() public IActionResult GetRemoteLoginLink()
{ {
@@ -130,13 +140,39 @@ namespace CarCareTracker.Controllers
Content = new FormUrlEncodedContent(httpParams) Content = new FormUrlEncodedContent(httpParams)
}; };
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync(); 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)) if (!string.IsNullOrWhiteSpace(userJwt))
{ {
//validate JWT token //validate JWT token
var tokenParser = new JwtSecurityTokenHandler(); var tokenParser = new JwtSecurityTokenHandler();
var parsedToken = tokenParser.ReadJwtToken(userJwt); 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)) if (!string.IsNullOrWhiteSpace(userEmailAddress))
{ {
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
@@ -180,6 +216,126 @@ namespace CarCareTracker.Controllers
} }
return new RedirectResult("/Login"); 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] [HttpPost]
public IActionResult Login(LoginModel credentials) public IActionResult Login(LoginModel credentials)
{ {

View File

@@ -16,6 +16,176 @@ namespace CarCareTracker.Controllers
{ {
return PartialView("_BulkDataImporter", mode); 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))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode) public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)

View File

@@ -64,8 +64,9 @@ namespace CarCareTracker.Controllers
[HttpGet] [HttpGet]
public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId) public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId)
{ {
var result = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
result.RemoveAll(x => !x.IsRecurring); result.RemoveAll(x => !x.IsRecurring);
result = result.OrderByDescending(x => x.Urgency).ThenBy(x => x.Description).ToList();
return PartialView("_RecurringReminderSelector", result); return PartialView("_RecurringReminderSelector", result);
} }
[HttpPost] [HttpPost]

View File

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

View File

@@ -1129,6 +1129,7 @@ namespace CarCareTracker.Controllers
{ {
var existingPreference = existingUserColumnPreference.Single(); var existingPreference = existingUserColumnPreference.Single();
existingPreference.VisibleColumns = columnPreference.VisibleColumns; existingPreference.VisibleColumns = columnPreference.VisibleColumns;
existingPreference.ColumnOrder = columnPreference.ColumnOrder;
} }
else else
{ {

12
Enum/ExtraFieldType.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public enum ExtraFieldType
{
Text = 0,
Number = 1,
Decimal = 2,
Date = 3,
Time = 4,
Location = 5
}
}

View File

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

View File

@@ -6,6 +6,7 @@ namespace CarCareTracker.Helper
public interface IFileHelper public interface IFileHelper
{ {
string GetFullFilePath(string currentFilePath, bool mustExist = true); string GetFullFilePath(string currentFilePath, bool mustExist = true);
byte[] GetFileBytes(string fullFilePath, bool deleteFile = false);
string MoveFileFromTemp(string currentFilePath, string newFolder); string MoveFileFromTemp(string currentFilePath, string newFolder);
bool RenameFile(string currentFilePath, string newName); bool RenameFile(string currentFilePath, string newName);
bool DeleteFile(string currentFilePath); bool DeleteFile(string currentFilePath);
@@ -85,6 +86,19 @@ namespace CarCareTracker.Helper
return string.Empty; 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) public bool RestoreBackup(string fileName, bool clearExisting = false)
{ {
var fullFilePath = GetFullFilePath(fileName); var fullFilePath = GetFullFilePath(fileName);

View File

@@ -11,11 +11,13 @@ namespace CarCareTracker.Helper
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token); OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token); OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders); OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
OperationResponse SendTestEmail(string emailAddress);
} }
public class MailHelper : IMailHelper public class MailHelper : IMailHelper
{ {
private readonly MailConfig mailConfig; private readonly MailConfig mailConfig;
private readonly string serverLanguage; private readonly string serverLanguage;
private readonly string serverDomain;
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
private readonly ITranslationHelper _translator; private readonly ITranslationHelper _translator;
private readonly ILogger<MailHelper> _logger; private readonly ILogger<MailHelper> _logger;
@@ -28,6 +30,7 @@ namespace CarCareTracker.Helper
//load mailConfig from Configuration //load mailConfig from Configuration
mailConfig = config.GetMailConfig(); mailConfig = config.GetMailConfig();
serverLanguage = config.GetServerLanguage(); serverLanguage = config.GetServerLanguage();
serverDomain = config.GetServerDomain();
_fileHelper = fileHelper; _fileHelper = fileHelper;
_translator = translationHelper; _translator = translationHelper;
_logger = logger; _logger = logger;
@@ -42,7 +45,14 @@ namespace CarCareTracker.Helper
return OperationResponse.Failed("Email Address or Token is invalid"); return OperationResponse.Failed("Email Address or Token is invalid");
} }
string emailSubject = _translator.Translate(serverLanguage, "Your Registration Token for LubeLogger"); 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); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {
@@ -63,7 +73,36 @@ namespace CarCareTracker.Helper
return OperationResponse.Failed("Email Address or Token is invalid"); return OperationResponse.Failed("Email Address or Token is invalid");
} }
string emailSubject = _translator.Translate(serverLanguage, "Your Password Reset Token for LubeLogger"); 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); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {

View File

@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
/// </summary> /// </summary>
public static class StaticHelper public static class StaticHelper
{ {
public const string VersionNumber = "1.4.4"; public const string VersionNumber = "1.4.8";
public const string DbName = "data/cartracker.db"; public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "data/config/userConfig.json"; public const string UserConfigPath = "data/config/userConfig.json";
public const string LegacyUserConfigPath = "config/userConfig.json"; public const string LegacyUserConfigPath = "config/userConfig.json";
@@ -22,6 +22,7 @@ namespace CarCareTracker.Helper
public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx"; public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json"; public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public const string TranslationPath = "https://hargata.github.io/lubelog_translations"; public const string TranslationPath = "https://hargata.github.io/lubelog_translations";
public const string ReleasePath = "https://api.github.com/repos/hargata/lubelog/releases/latest";
public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json"; public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com"; public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input) public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
@@ -262,7 +263,9 @@ namespace CarCareTracker.Helper
//update isrequired setting //update isrequired setting
foreach (ExtraField extraField in recordExtraFields) 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 //append extra fields
foreach (ExtraField extraField in templateExtraFields) foreach (ExtraField extraField in templateExtraFields)

View File

@@ -303,11 +303,11 @@ namespace CarCareTracker.Logic
//set next reminder //set next reminder
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date)) 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)) 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); apiResult.Add(resultToAdd);
} }

View File

@@ -75,7 +75,9 @@ namespace CarCareTracker.Middleware
var userIdentity = new List<Claim> var userIdentity = new List<Claim>
{ {
new(ClaimTypes.Name, splitString[0]), 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) if (userData.IsAdmin)
{ {

View File

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

View File

@@ -8,13 +8,14 @@
public string AuthURL { get; set; } public string AuthURL { get; set; }
public string TokenURL { get; set; } public string TokenURL { get; set; }
public string RedirectURL { 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 State { get; set; }
public string CodeChallenge { get; set; } public string CodeChallenge { get; set; }
public bool ValidateState { get; set; } = false; public bool ValidateState { get; set; } = false;
public bool DisableRegularLogin { get; set; } = false; public bool DisableRegularLogin { get; set; } = false;
public bool UsePKCE { get; set; } = false; public bool UsePKCE { get; set; } = false;
public string LogOutURL { get; set; } = ""; public string LogOutURL { get; set; } = "";
public string UserInfoURL { get; set; } = "";
public string RemoteAuthURL { get { public string RemoteAuthURL { get {
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
if (UsePKCE) if (UsePKCE)

View File

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

View File

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

View File

@@ -13,5 +13,6 @@
public decimal Cost { get; set; } public decimal Cost { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
} }
} }

View File

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

View File

@@ -9,5 +9,6 @@
public bool FilterByDateRange { get; set; } = false; public bool FilterByDateRange { get; set; } = false;
public string StartDate { get; set; } = ""; public string StartDate { get; set; } = "";
public string EndDate { get; set; } = ""; public string EndDate { get; set; } = "";
public bool PrintIndividualRecords { get; set; } = false;
} }
} }

View File

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

View 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();
}
}

View File

@@ -5,5 +5,6 @@
public string Name { get; set; } public string Name { get; set; }
public string Value { get; set; } public string Value { get; set; }
public bool IsRequired { get; set; } public bool IsRequired { get; set; }
public ExtraFieldType FieldType { get; set; } = ExtraFieldType.Text;
} }
} }

View File

@@ -115,6 +115,8 @@ namespace CarCareTracker.Models
} }
public class ReminderExportModel public class ReminderExportModel
{ {
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Urgency { get; set; } public string Urgency { get; set; }
public string Metric { get; set; } public string Metric { get; set; }
@@ -123,17 +125,33 @@ namespace CarCareTracker.Models
public string DueDate { get; set; } public string DueDate { get; set; }
[JsonConverter(typeof(FromIntOptional))] [JsonConverter(typeof(FromIntOptional))]
public string DueOdometer { get; set; } public string DueOdometer { get; set; }
public string Tags { get; set; }
} }
public class PlanRecordExportModel public class PlanRecordExportModel
{ {
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateCreated { get; set; } public string DateCreated { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateModified { get; set; } public string DateModified { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Type { get; set; } public string Type { get; set; }
public string Priority { get; set; } public string Priority { get; set; }
public string Progress { get; set; } public string Progress { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; } public string Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); 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; }
} }
} }

View File

@@ -4,5 +4,6 @@
{ {
public ImportMode Tab { get; set; } public ImportMode Tab { get; set; }
public List<string> VisibleColumns { get; set; } = new List<string>(); public List<string> VisibleColumns { get; set; } = new List<string>();
public List<string> ColumnOrder { get; set; } = new List<string>();
} }
} }

View File

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

View File

@@ -1,4 +1,6 @@
namespace CarCareTracker.Models using System.Text.Json.Serialization;
namespace CarCareTracker.Models
{ {
public class Vehicle public class Vehicle
{ {
@@ -8,7 +10,9 @@
public string Make { get; set; } public string Make { get; set; }
public string Model { get; set; } public string Model { get; set; }
public string LicensePlate { get; set; } public string LicensePlate { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string PurchaseDate { get; set; } public string PurchaseDate { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string SoldDate { get; set; } public string SoldDate { get; set; }
public decimal PurchasePrice { get; set; } public decimal PurchasePrice { get; set; }
public decimal SoldPrice { get; set; } public decimal SoldPrice { get; set; }
@@ -22,10 +26,12 @@
/// <summary> /// <summary>
/// Primarily used for vehicles with odometer units different from user's settings. /// Primarily used for vehicles with odometer units different from user's settings.
/// </summary> /// </summary>
[JsonConverter(typeof(FromDecimalOptional))]
public string OdometerMultiplier { get; set; } = "1"; public string OdometerMultiplier { get; set; } = "1";
/// <summary> /// <summary>
/// Primarily used for vehicles where the odometer does not reflect actual mileage. /// Primarily used for vehicles where the odometer does not reflect actual mileage.
/// </summary> /// </summary>
[JsonConverter(typeof(FromIntOptional))]
public string OdometerDifference { get; set; } = "0"; public string OdometerDifference { get; set; } = "0";
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>(); public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
/// <summary> /// <summary>

View File

@@ -26,6 +26,34 @@
<h6>Parameters</h6> <h6>Parameters</h6>
</div> </div>
</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>
</div>
<div class="col-5 copyable testable">
<code>/api/version</code>
</div>
<div class="col-3">
Returns current version of LubeLogger and checks for updates
</div>
<div class="col-3">
CheckForUpdate(bool) - checks for update(optional)
</div>
</div>
<div class="row api-method"> <div class="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
@@ -159,6 +187,83 @@
Id - Id of Odometer Record Id - Id of Odometer Record
</div> </div>
</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="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
@@ -408,7 +513,7 @@
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/vehicle/taxrecords/check</code> <code>/api/vehicle/taxrecords/check</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -578,6 +683,65 @@
vehicleId - Id of Vehicle vehicleId - Id of Vehicle
</div> </div>
</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="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
@model KioskViewModel @model KioskViewModel
@section Scripts { @section Scripts {
<script src="~/lib/masonry/masonry.min.js"></script> <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" 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> <div class="progress-bar" style="width: 0%"></div>
@@ -123,9 +124,15 @@
} }
function toggleReminderNote(sender){ function toggleReminderNote(sender){
var reminderNote = $(sender).find('.reminder-note'); var reminderNote = $(sender).find('.reminder-note');
if (reminderNote.text().trim() != ''){ var reminderNoteText = reminderNote.text().trim();
if (reminderNoteText != ''){
if (reminderNote.hasClass('d-none')) { if (reminderNote.hasClass('d-none')) {
reminderNote.removeClass('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 { } else {
reminderNote.addClass('d-none'); reminderNote.addClass('d-none');
} }

View File

@@ -36,8 +36,9 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <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-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> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
@@ -47,11 +48,21 @@
@foreach (ExtraField extraField in Model.ExtraFields) @foreach (ExtraField extraField in Model.ExtraFields)
{ {
<script> <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> </script>
<tr class="d-flex"> <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-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> <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> </tr>
} }
@@ -99,6 +110,12 @@
extraFieldToEdit.isRequired = $(checkbox).is(":checked"); extraFieldToEdit.isRequired = $(checkbox).is(":checked");
updateExtraFields(); 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) { function deleteExtraField(fieldId) {
extraFields = extraFields.filter(x => x.name != fieldId); extraFields = extraFields.filter(x => x.name != fieldId);
updateExtraFields(); updateExtraFields();

View File

@@ -81,9 +81,9 @@
</div> </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%;"> <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> </div>
</div> </div>

View 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>

View File

@@ -86,6 +86,14 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies"> <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> <label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label>
</div> </div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showCalendar" checked="@Model.UserConfig.ShowCalendar">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Calendar")</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showVehicleThumbnail" checked="@Model.UserConfig.ShowVehicleThumbnail">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Vehicle Thumbnail in Header")</label>
</div>
</div> </div>
@if (User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
@@ -253,7 +261,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <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="row">
<div class="col-6 d-grid"> <div class="col-6 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button> <button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button>
@@ -355,6 +370,12 @@
</div> </div>
</div> </div>
</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 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-dialog modal-lg" role="document">
<div class="modal-content" id="tabReorderModalContent"> <div class="modal-content" id="tabReorderModalContent">

View File

@@ -16,7 +16,7 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label> <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>
<div class="d-grid"> <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> <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>

View File

@@ -24,7 +24,7 @@
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<div class="input-group"> <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"> <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> <button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div> </div>

View File

@@ -34,7 +34,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label> <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>
<div class="d-grid"> <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> <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>

View File

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

View 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>

View File

@@ -4,6 +4,7 @@
@{ @{
var userLanguage = config.GetServerLanguage(); var userLanguage = config.GetServerLanguage();
} }
@model LoginModel
@{ @{
ViewData["Title"] = "Reset Password"; ViewData["Title"] = "Reset Password";
} }
@@ -16,16 +17,16 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group"> <div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label> <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>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Email Address")</label> <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>
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label>
<div class="input-group"> <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"> <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> <button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div> </div>

View File

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

View File

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

View File

@@ -21,30 +21,7 @@
<div class="alert alert-danger" role="alert"> <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.") @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> </div>
@if (Model == ImportMode.GasRecord) <a class="btn btn-link" href="@($"/Vehicle/GenerateCsvSample?mode={Model.ToString()}")" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
{
<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>
}
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <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>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,37 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -93,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View 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>
}
}

View File

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

View File

@@ -12,7 +12,7 @@
{ {
<li class="list-group-item"> <li class="list-group-item">
<div class="d-flex justify-content-between"> <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"> <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-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> <button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>

View File

@@ -104,55 +104,55 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_DateRefueled">@translator.Translate(userLanguage, "Date Refueled")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Delta">@translator.Translate(userLanguage, "Delta")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Consumption">@translator.Translate(userLanguage, "Consumption")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_FuelEconomy">@translator.Translate(userLanguage, "Fuel Economy")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_UnitCost">@translator.Translate(userLanguage, "Unit Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -161,7 +161,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

@@ -97,14 +97,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.GasRecord.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,14 +72,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,37 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_InitialOdometer">@translator.Translate(userLanguage, "Initial Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Distance">@translator.Translate(userLanguage, "Distance")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -93,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

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

View File

@@ -40,14 +40,7 @@
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option> <!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> <!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
@if (!isNew) @if (!isNew)
{ {
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label> <label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>

View File

@@ -40,14 +40,7 @@
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option> <!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> <!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="row swimlane"> <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="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Planned")</span> <span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
@@ -59,7 +59,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord) @await Html.PartialAsync("_PlanRecordItem", planRecord)
} }
</div> </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="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Doing")</span> <span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
@@ -81,7 +81,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord) @await Html.PartialAsync("_PlanRecordItem", planRecord)
} }
</div> </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="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Done")</span> <span class="display-7">@translator.Translate(userLanguage,"Done")</span>

View File

@@ -5,7 +5,7 @@
var userLanguage = userConfig.UserLanguage; var userLanguage = userConfig.UserLanguage;
} }
@using CarCareTracker.Helper @using CarCareTracker.Helper
@model List<ReminderRecord> @model List<ReminderRecordViewModel>
@if (Model.Count() > 1) @if (Model.Count() > 1)
{ {
<div class="mb-2"> <div class="mb-2">
@@ -16,9 +16,25 @@
<select class="form-select" id="recurringReminderInput"> <select class="form-select" id="recurringReminderInput">
@if (Model.Any()) @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 } else
{ {
@@ -27,11 +43,26 @@
</select> </select>
<div id="recurringMultipleReminders" style="display:none;"> <div id="recurringMultipleReminders" style="display:none;">
<ul class="list-group"> <ul class="list-group">
@foreach (ReminderRecord reminderRecord in Model) @foreach (ReminderRecordViewModel reminderRecord in Model)
{ {
<li class="list-group-item text-start"> <li class="list-group-item text-start">
<input class="form-check-input" type="checkbox" value="@reminderRecord.Id" id="recurringReminder_@reminderRecord.Id"> <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</label> <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> </li>
} }
</ul> </ul>

View File

@@ -85,6 +85,20 @@
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div> </div>
</li> </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> </ul>
</div> </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> <th scope="col" data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@translator.Translate(userLanguage, "Notes")</th>
@if (hasRefresh) @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> </tr>
</thead> </thead>
<tbody> <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> <td data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@StaticHelper.TruncateStrings(reminderRecord.Notes)</td>
@if (hasRefresh) @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) @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> <button type="button" class="btn btn-secondary" onclick="markDoneReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-check-lg"></i></button>
} }
</td> </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> <button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button>
</td> </td>
</tr> </tr>

View File

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

View File

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

View File

@@ -25,6 +25,14 @@
} }
</ul> </ul>
</div> </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"> <div class="mt-2 mb-2">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item text-center" style="cursor:pointer;" onclick="showReportAdvancedParameters()"> <li class="list-group-item text-center" style="cursor:pointer;" onclick="showReportAdvancedParameters()">

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <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>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,37 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -93,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

@@ -50,14 +50,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,49 +54,49 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_PartNumber">@translator.Translate(userLanguage, "Part Number")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Supplier">@translator.Translate(userLanguage, "Supplier")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Quantity">@translator.Translate(userLanguage, "Quantity")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -105,7 +105,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

@@ -28,7 +28,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <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>
</div> </div>
} }
@@ -41,14 +41,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,31 +54,31 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -87,7 +87,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <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>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
</div> </div>
<div class="col-md-6 col-12"> <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> <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>

View File

@@ -54,37 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </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"> <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"> <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> <label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div> </div>
</li> </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"> <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> <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> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -93,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); 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"> <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"> <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> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>

View File

@@ -12,7 +12,7 @@
{ {
<li class="list-group-item"> <li class="list-group-item">
<div class="d-flex justify-content-between"> <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"> <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-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> <button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>

View File

@@ -1,9 +1,13 @@
@model IEnumerable<UserColumnPreference> @model IEnumerable<UserColumnPreference>
<script> <script>
var visibleColumns = []; var visibleColumns = [];
var columnsOrder = [];
@foreach(string visibleColumn in Model.SelectMany(x=> x.VisibleColumns)) @foreach(string visibleColumn in Model.SelectMany(x=> x.VisibleColumns))
{ {
@:visibleColumns.push(decodeHTMLEntities('@visibleColumn')); @:visibleColumns.push(decodeHTMLEntities('@visibleColumn'));
} }
loadUserColumnPreferences(visibleColumns); @foreach(string columnOrder in Model.SelectMany(x=>x.ColumnOrder)){
@:columnsOrder.push(decodeHTMLEntities('@columnOrder'));
}
loadUserColumnPreferences(visibleColumns, columnsOrder);
</script> </script>

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage; var userLanguage = userConfig.UserLanguage;
var extraFields = Model.ReportParameters.ExtraFields; var extraFields = Model.ReportParameters.ExtraFields;
} }
<div> <div style="page-break-after: always;">
<div class="row mt-2"> <div class="row mt-2">
<div class="d-flex"> <div class="d-flex">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
@@ -18,13 +18,12 @@
</span> </span>
@if (!string.IsNullOrWhiteSpace(Model.StartDate) && !string.IsNullOrWhiteSpace(Model.EndDate)) @if (!string.IsNullOrWhiteSpace(Model.StartDate) && !string.IsNullOrWhiteSpace(Model.EndDate))
{ {
<br /> <br />
<span class="lead ms-2"> <span class="lead ms-2">
@($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}") @($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}")
</span> </span>
} }
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
@@ -93,26 +92,26 @@
<hr /> <hr />
@if (Model.TotalDepreciation != default) @if (Model.TotalDepreciation != default)
{ {
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
@(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation")) @(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> </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 /> <hr />
} }
<div class="row"> <div class="row">
@@ -177,3 +176,113 @@
</div> </div>
</div> </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>
}

View File

@@ -36,14 +36,7 @@
<input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model"> <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> <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"> <input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", 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>
}
<label for="inputIdentifier" class="@(!Model.ExtraFields.Any() ? "d-none" : "")">@translator.Translate(userLanguage, "Identifier")</label> <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" )> <select class="form-select @(!Model.ExtraFields.Any() ? "d-none" : "")" id="inputIdentifier" )>
<!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option> <!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option>
@@ -89,9 +82,15 @@
</div> </div>
<div id="collapsePurchaseInfo" class="accordion-collapse collapse" data-bs-parent="#vehicleModalAccordion"> <div id="collapsePurchaseInfo" class="accordion-collapse collapse" data-bs-parent="#vehicleModalAccordion">
<label for="inputPurchaseDate">@translator.Translate(userLanguage, "Purchased Date(optional)")</label> <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> <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> <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)"> <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> <label for="inputSoldPrice">@translator.Translate(userLanguage, "Sold Price(optional)")</label>

View File

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

View File

@@ -2,7 +2,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <title>LubeLogger Configurator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <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"> <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"> <input type="text" id="inputSmtpFrom" class="form-control">
<small class="text-body-secondary">Sender Email Address</small> <small class="text-body-secondary">Sender Email Address</small>
</div> </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> </div>
</div> </div>
@@ -150,6 +155,11 @@
<input type="text" id="inputOIDCTokenURL" class="form-control"> <input type="text" id="inputOIDCTokenURL" class="form-control">
<small class="text-body-secondary">Token URL from Provider</small> <small class="text-body-secondary">Token URL from Provider</small>
</div> </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"> <div class="form-group">
<label for="inputOIDCRedirectURL">LubeLogger URL</label> <label for="inputOIDCRedirectURL">LubeLogger URL</label>
<input type="text" id="inputOIDCRedirectURL" class="form-control"> <input type="text" id="inputOIDCRedirectURL" class="form-control">
@@ -227,7 +237,9 @@
<textarea id="outputModalText" readonly style="width:100%; height:450px;"></textarea> <textarea id="outputModalText" readonly style="width:100%; height:450px;"></textarea>
</div> </div>
<div class="modal-footer"> <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-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-copy" onclick="copyToClipboard()">Copy</button> <button type="button" class="btn btn-primary btn-copy" onclick="copyToClipboard()">Copy</button>
</div> </div>
@@ -237,6 +249,44 @@
</div> </div>
</body> </body>
<script> <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(){ function removeDoubleQuotes(){
var currentText = $("#outputModalText").text(); var currentText = $("#outputModalText").text();
$("#outputModalText").text(currentText.replaceAll('"', '')); $("#outputModalText").text(currentText.replaceAll('"', ''));
@@ -276,6 +326,9 @@ function generateConfig(){
if ($("#inputInvariantAPI").is(":checked")){ if ($("#inputInvariantAPI").is(":checked")){
windowConfig["LUBELOGGER_INVARIANT_API"]=$('#inputInvariantAPI').is(':checked'); windowConfig["LUBELOGGER_INVARIANT_API"]=$('#inputInvariantAPI').is(':checked');
} }
if ($("#inputServerDomain").val().trim() != ''){
windowConfig["LUBELOGGER_DOMAIN"] = $('#inputServerDomain').val();
}
if ($('#inputSmtpServer').val().trim() != ''){ if ($('#inputSmtpServer').val().trim() != ''){
windowConfig["MailConfig"] = { windowConfig["MailConfig"] = {
EmailServer: $("#inputSmtpServer").val(), EmailServer: $("#inputSmtpServer").val(),
@@ -292,6 +345,7 @@ function generateConfig(){
ClientSecret: $("#inputOIDCClientSecret").val(), ClientSecret: $("#inputOIDCClientSecret").val(),
AuthURL: $("#inputOIDCAuthURL").val(), AuthURL: $("#inputOIDCAuthURL").val(),
TokenURL: $("#inputOIDCTokenURL").val(), TokenURL: $("#inputOIDCTokenURL").val(),
UserInfoURL: $("#inputOIDCUserInfoURL").val(),
RedirectURL: redirectUrl, RedirectURL: redirectUrl,
Scope: $("#inputOIDCScope").val(), Scope: $("#inputOIDCScope").val(),
ValidateState: $("#inputOIDCValidateState").is(":checked"), ValidateState: $("#inputOIDCValidateState").is(":checked"),
@@ -319,6 +373,11 @@ function generateConfig(){
$("#outputModalLabel").text("Append into appsettings.json"); $("#outputModalLabel").text("Append into appsettings.json");
$("#outputModalText").text(JSON.stringify(windowConfig, null, 2).slice(1,-1)); $("#outputModalText").text(JSON.stringify(windowConfig, null, 2).slice(1,-1));
$(".btn-strip").hide(); $(".btn-strip").hide();
if (jQuery.isEmptyObject(windowConfig)){
$(".btn-upload").hide();
} else {
$(".btn-upload").show();
}
$("#outputModal").modal("show"); $("#outputModal").modal("show");
} else { } else {
var dockerConfig = []; var dockerConfig = [];
@@ -341,6 +400,9 @@ function generateConfig(){
if ($("#inputPostgres").val().trim() != ''){ if ($("#inputPostgres").val().trim() != ''){
dockerConfig.push(`POSTGRES_CONNECTION="${$('#inputPostgres').val()}"`); dockerConfig.push(`POSTGRES_CONNECTION="${$('#inputPostgres').val()}"`);
} }
if ($("#inputServerDomain").val().trim() != ''){
dockerConfig.push(`LUBELOGGER_DOMAIN="${$('#inputServerDomain').val()}"`);
}
if ($("#inputCustomWidgets").is(":checked")){ if ($("#inputCustomWidgets").is(":checked")){
dockerConfig.push(`LUBELOGGER_CUSTOM_WIDGETS="${$('#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__ClientSecret="${$('#inputOIDCClientSecret').val()}"`);
dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`); dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`);
dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`); dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`);
dockerConfig.push(`OpenIDConfig__UserInfoURL="${$('#inputOIDCUserInfoURL').val()}"`);
dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`); dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`);
dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`); dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`);
dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`); dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`);
@@ -375,6 +438,7 @@ function generateConfig(){
$("#outputModalLabel").text("Content for .env"); $("#outputModalLabel").text("Content for .env");
$("#outputModalText").text(dockerConfig.join("\r\n")); $("#outputModalText").text(dockerConfig.join("\r\n"));
$(".btn-strip").show(); $(".btn-strip").show();
$(".btn-upload").hide();
$("#outputModal").modal("show"); $("#outputModal").modal("show");
} }
} }

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -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 Date Odometer FuelConsumed Cost IsFillToFull MissedFuelUp Notes
2 5/8/2020 204836 8.331 16.24 True False
3 5/30/2020 205056 11.913 25.72 True False

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,2 +0,0 @@
Date,Odometer,Notes
1/1/2024,260001,test test
1 Date Odometer Notes
2 1/1/2024 260001 test test

View File

@@ -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 DateCreated DateModified Description Notes Type Priority Progress Cost
2 1/19/2024 6:01:02 PM 1/19/2024 7:32:58 PM Repair Exhaust RepairRecord Normal Testing $50.00

View File

@@ -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 Date Odometer Description Notes Cost
2 01/01/2020 45000 Test Description 1 Test Note 1 20.60
3 01/02/2020 47000 Test Description 2 Test Note 2 40.45

View File

@@ -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 Date PartNumber PartSupplier PartQuantity Description Cost Notes
2 1/16/2024 ‎EVA17872045551 Evan Fischer 1 Front Bumper $95.14 Altima Activities

View File

@@ -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
1 Date Description Notes Cost
2 01/01/2020 Test Description 1 Test Note 1 20.60
3 01/02/2020 Test Description 2 Test Note 2 40.45

View File

@@ -319,15 +319,17 @@ function updateMPGLabels() {
var rowsToAggregate = $("[data-aggregated='true']").parent(":not('.override-hide')"); var rowsToAggregate = $("[data-aggregated='true']").parent(":not('.override-hide')");
var rowsUnaggregated = $("[data-aggregated='false']").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 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 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 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 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 totalUnaggregatedGasConsumed = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).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 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; var fullDistanceTraveled = totalMilesTraveled + totalMilesTraveledUnaggregated;
if (totalGasConsumed > 0) { if (totalGasConsumed > 0 && rowNonZeroMPG.length > 0) {
var averageMPG = totalMilesTraveled / totalGasConsumed; var averageMPG = totalMilesTraveled / totalGasConsumed;
if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l' && averageMPG > 0) { if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l' && averageMPG > 0) {
averageMPG = 100 / averageMPG; averageMPG = 100 / averageMPG;

View File

@@ -55,12 +55,6 @@ function performPasswordReset() {
}); });
} }
function handlePasswordKeyPress(event) {
if (event.keyCode == 13) {
performLogin();
}
}
function remoteLogin() { function remoteLogin() {
$.get('/Login/GetRemoteLoginLink', function (data) { $.get('/Login/GetRemoteLoginLink', function (data) {
if (data) { if (data) {

View File

@@ -7,6 +7,7 @@ function getAndValidateSelectedColumns() {
var tagFilterMode = $("#tagSelector").val(); var tagFilterMode = $("#tagSelector").val();
var tagsToFilter = $("#tagSelectorInput").val(); var tagsToFilter = $("#tagSelectorInput").val();
var filterByDateRange = $("#dateRangeSelector").is(":checked"); var filterByDateRange = $("#dateRangeSelector").is(":checked");
var printIndividualRecords = $("#printIndividualRecordsCheck").is(":checked");
var startDate = $("#dateRangeStartDate").val(); var startDate = $("#dateRangeStartDate").val();
var endDate = $("#dateRangeEndDate").val(); var endDate = $("#dateRangeEndDate").val();
$("#columnSelector :checked").map(function () { $("#columnSelector :checked").map(function () {
@@ -42,7 +43,8 @@ function getAndValidateSelectedColumns() {
tags: [], tags: [],
filterByDateRange: filterByDateRange, filterByDateRange: filterByDateRange,
startDate: '', startDate: '',
endDate: '' endDate: '',
printIndividualRecords: printIndividualRecords
} }
} else { } else {
return { return {
@@ -54,7 +56,8 @@ function getAndValidateSelectedColumns() {
tags: tagsToFilter, tags: tagsToFilter,
filterByDateRange: filterByDateRange, filterByDateRange: filterByDateRange,
startDate: startDate, startDate: startDate,
endDate: endDate endDate: endDate,
printIndividualRecords: printIndividualRecords
} }
} }
} }
@@ -80,6 +83,7 @@ function getSavedReportParameters() {
$("#dateRangeSelector").prop('checked', selectedReportColumns.filterByDateRange); $("#dateRangeSelector").prop('checked', selectedReportColumns.filterByDateRange);
$("#dateRangeStartDate").val(selectedReportColumns.startDate); $("#dateRangeStartDate").val(selectedReportColumns.startDate);
$("#dateRangeEndDate").val(selectedReportColumns.endDate); $("#dateRangeEndDate").val(selectedReportColumns.endDate);
$("#printIndividualRecordsCheck").prop('checked', selectedReportColumns.printIndividualRecords);
} }
} }
function generateVehicleHistoryReport() { function generateVehicleHistoryReport() {
@@ -139,6 +143,14 @@ function refreshMPGChart() {
var year = getYear(); var year = getYear();
$.post('/Vehicle/GetMonthMPGByVehicle', {vehicleId: vehicleId, year: year}, function (data) { $.post('/Vehicle/GetMonthMPGByVehicle', {vehicleId: vehicleId, year: year}, function (data) {
$("#monthFuelMileageReportContent").html(data); $("#monthFuelMileageReportContent").html(data);
refreshReportHeader();
})
}
function refreshReportHeader() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.post('/Vehicle/GetSummaryForVehicle', { vehicleId: vehicleId, year: year }, function (data) {
$("#reportHeaderContent").html(data);
}) })
} }
function setSelectedMetrics() { function setSelectedMetrics() {

View File

@@ -4,6 +4,15 @@
$("#extraFieldModal").modal('show'); $("#extraFieldModal").modal('show');
}); });
} }
function showServerConfigModal() {
$.get(`/Home/GetServerConfiguration`, function (data) {
$("#serverConfigModalContent").html(data);
$("#serverConfigModal").modal('show');
});
}
function hideServerConfigModal() {
$("#serverConfigModal").modal('hide');
}
function hideExtraFieldModal() { function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide'); $("#extraFieldModal").modal('hide');
} }
@@ -62,6 +71,8 @@ function updateSettings() {
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"), enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"), enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"), enableShopSupplies: $("#enableShopSupplies").is(":checked"),
showCalendar: $("#showCalendar").is(":checked"),
showVehicleThumbnail: $("#showVehicleThumbnail").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"), enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"), hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(), preferredGasUnit: $("#preferredGasUnit").val(),
@@ -85,6 +96,33 @@ function updateSettings() {
} }
}) })
} }
function sendTestEmail() {
Swal.fire({
title: 'Send Test Email',
html: `
<input type="text" id="testEmailRecipient" class="swal2-input" placeholder="Email Address" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Send',
focusConfirm: false,
preConfirm: () => {
const emailRecipient = $("#testEmailRecipient").val();
if (!emailRecipient || emailRecipient.trim() == '') {
Swal.showValidationMessage(`Please enter a valid email address`);
}
return { emailRecipient }
},
}).then(function (result) {
if (result.isConfirmed) {
$.post('/Home/SendTestEmail', { emailAddress: result.value.emailRecipient }, function (data) {
if (data.success) {
successToast(data.message);
} else {
errorToast(data.message);
}
});
}
});
}
function makeBackup() { function makeBackup() {
$.get('/Files/MakeBackup', function (data) { $.get('/Files/MakeBackup', function (data) {
window.location.href = data; window.location.href = data;

View File

@@ -1,4 +1,7 @@
function successToast(message) { function returnToGarage() {
window.location.href = '/Home';
}
function successToast(message) {
Swal.fire({ Swal.fire({
toast: true, toast: true,
position: "top-end", position: "top-end",
@@ -336,6 +339,16 @@ function isValidMoney(input) {
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/; const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
return (euRegex.test(input) || usRegex.test(input)); 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) { function initDatePicker(input, futureOnly) {
if (futureOnly) { if (futureOnly) {
input.datepicker({ input.datepicker({
@@ -702,7 +715,7 @@ function getAndValidateExtraFields() {
var extraFieldsVisible = $(".modal.fade.show").find(".extra-field"); var extraFieldsVisible = $(".modal.fade.show").find(".extra-field");
extraFieldsVisible.map((index, elem) => { extraFieldsVisible.map((index, elem) => {
var extraFieldName = $(elem).children("label").text(); var extraFieldName = $(elem).children("label").text();
var extraFieldInput = $(elem).children("input"); var extraFieldInput = $(elem).find("input");
var extraFieldValue = extraFieldInput.val(); var extraFieldValue = extraFieldInput.val();
var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required'); var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required');
if (extraFieldIsRequired && extraFieldValue.trim() == '') { if (extraFieldIsRequired && extraFieldValue.trim() == '') {
@@ -1370,7 +1383,7 @@ function searchTableRows(tabName) {
} }
}); });
} }
function loadUserColumnPreferences(columns) { function loadUserColumnPreferences(columns, order) {
if (columns.length == 0) { if (columns.length == 0) {
//user has no preference saved, reset to default //user has no preference saved, reset to default
return; return;
@@ -1387,12 +1400,35 @@ function loadUserColumnPreferences(columns) {
$(`[data-column='${x}']`).show(); $(`[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) { function saveUserColumnPreferences(importMode) {
var visibleColumns = $('.col-visible-toggle:checked').map((index, elem) => $(elem).attr('data-column-toggle')).toArray(); 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 = { var columnPreference = {
tab: importMode, tab: importMode,
visibleColumns: visibleColumns visibleColumns: visibleColumns,
columnOrder: columnOrder
}; };
$.post('/Vehicle/SaveUserColumnPreferences', { columnPreference: columnPreference }, function (data) { $.post('/Vehicle/SaveUserColumnPreferences', { columnPreference: columnPreference }, function (data) {
if (!data) { if (!data) {
@@ -1473,3 +1509,74 @@ function togglePasswordVisibility(elem) {
passwordButton.addClass('bi-eye'); 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');
}
}
}

Some files were not shown because too many files have changed in this diff Show More