Compare commits

...

95 Commits

Author SHA1 Message Date
Hargata Softworks
cfd83b5b4e Merge pull request #594 from hargata/Hargata/588
removed order by
2024-08-26 15:25:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e468261095 removed order by 2024-08-26 15:23:20 -06:00
Hargata Softworks
1362dc87df Merge pull request #593 from hargata/Hargata/588
Allow extra fields to be added via API
2024-08-26 15:13:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b89902bbdb Allow extra fields to be added via API 2024-08-26 15:12:05 -06:00
Hargata Softworks
f31b70a6dc Merge pull request #592 from hargata/Hargata/531
Display the due date and odometer in a tooltip in the reminder record…
2024-08-26 12:43:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
47a1f0c4d5 Display the due date and odometer in a tooltip in the reminder records view. 2024-08-26 12:38:42 -06:00
Hargata Softworks
3234b530d5 Merge pull request #591 from hargata/Hargata/reminder.api.enhancement
Add due date and due odometer to reminder output for API method.
2024-08-26 10:33:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
75d16544db Add due date and due odometer to reminder output for API method. 2024-08-26 10:32:14 -06:00
Hargata Softworks
7179b60116 Merge pull request #590 from hargata/Hargata/csv.extrafield.export
remove unused variable.
2024-08-23 13:45:22 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7cb5d8254f remove unused variable. 2024-08-23 13:44:58 -06:00
Hargata Softworks
6ab7ee6e4f Merge pull request #589 from hargata/Hargata/csv.extrafield.export
Add functionality to export extra fields in CSV exports.
2024-08-23 13:41:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a57ce55f8b Fix type 2024-08-23 13:41:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
190484762d Add functionality to export extra fields in CSV exports. 2024-08-23 13:39:08 -06:00
Hargata Softworks
78554ade5d Merge pull request #587 from hargata/Hargata/api.enhancement
Further Enhance Cleanup API Endpoint.
2024-08-22 11:13:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
364a13226a Added return object. 2024-08-22 11:11:17 -06:00
Hargata Softworks
691813838c Merge pull request #586 from hargata/Hargata/cleanup.api
Add API Controller method to clean up temp files and unlinked thumbnails
2024-08-21 21:24:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0defb0fadf json json. 2024-08-21 21:23:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
08bf00ee37 Add API Controller method to clean up temp files and unlinked thumbnails. 2024-08-21 21:21:24 -06:00
Hargata Softworks
522322ee2a Merge pull request #585 from hargata/Hargata/570
Upload files on paste.
2024-08-21 18:13:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
062e3600e7 update uploaded files in real time for records that already have uploaded files and fix note attachment bug 2024-08-21 18:11:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a92160f6d7 Upload files on paste. 2024-08-21 14:38:15 -06:00
Hargata Softworks
4adf967f7f Merge pull request #584 from hargata/Hargata/534
Add fuel type dropdown and diesel fuel type.
2024-08-19 12:13:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fd6bd98a25 Missing translation key. 2024-08-19 12:12:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fe76f32778 Add fuel type dropdown and diesel fuel type. 2024-08-19 12:09:50 -06:00
Hargata Softworks
897b4f2efe Merge pull request #581 from hargata/Hargata/576
Resolve Empty Tables Postgres Bug.
2024-08-14 09:27:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
267f903ffe Resolve bug where attempting to delete a vehicle with zero records returns an error. 2024-08-13 08:19:05 -06:00
Hargata Softworks
db884d0a6a Merge pull request #569 from hargata/Hargata/568
Add PKCE to OIDC Auth Flow
2024-07-31 12:28:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d1190e7ddb Update readme to add helm chart by anza labs. 2024-07-31 12:27:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dae8ab679e add better logging for when IdP returns errors. 2024-07-30 11:57:35 -06:00
DESKTOP-T0O5CDB\DESK-555BD
86ec9409c3 added pkce code for when user logins automatically. 2024-07-29 15:28:00 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2c4ddb0c38 Updated version. 2024-07-29 15:23:31 -06:00
DESKTOP-T0O5CDB\DESK-555BD
44d10f11ca Add PKCE functionality(RFC 7636) to OIDC functionality. 2024-07-29 15:23:10 -06:00
Hargata Softworks
6cf733b9c6 Merge pull request #558 from hargata/Hargata/545
enable extra fields to be imported via CSV
2024-07-09 14:52:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1eb6e2cedf enable extra fields to be imported via CSV 2024-07-09 14:48:09 -06:00
Hargata Softworks
ef4deaba8f Merge pull request #557 from hargata/Hargata/512
recurring interval are now based on reminder metric.
2024-07-09 11:16:09 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8c196c2fa recurring interval are now based on reminder metric. 2024-07-09 11:15:02 -06:00
Hargata Softworks
f7c9db6353 Merge pull request #556 from hargata/Hargata/mailconfig.cleanup
Clean up MailConfig and update npsql
2024-07-09 10:35:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4ce720ff97 Update npgSQL 2024-07-09 10:34:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a96011629b clean up mailconfig 2024-07-09 10:32:40 -06:00
Hargata Softworks
9dcdcf97e8 Merge pull request #508 from snpaul22/main
Create app schema automatically
2024-07-06 17:40:13 -06:00
snpaul22
5148338f52 Merge branch 'hargata:main' into main 2024-07-06 18:54:14 -04:00
Hargata Softworks
0d3c04d8f8 Merge pull request #554 from hargata/Hargata/root.user.update
Make it easier to update root user credentials and patch bug.
2024-07-05 11:26:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
15328a14b4 Make it easier to update root user credentials and patch bug. 2024-07-05 11:18:46 -06:00
Hargata Softworks
2d092f722a Merge pull request #550 from hargata/Hargata/disabled.init.odo
updated button color.
2024-07-02 15:05:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8825cb9b9b updated button color. 2024-07-02 15:05:16 -06:00
Hargata Softworks
2f17e303ab Merge pull request #549 from hargata/Hargata/disabled.init.odo
Disable the initial odometer reading field
2024-07-02 11:51:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4b56c8a343 a better implementation of disabled attribute 2024-07-02 11:09:41 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7b6b62c623 Disable the initial odometer reading field if it's non-zero to prevent accidental editing. 2024-07-02 10:54:11 -06:00
Hargata Softworks
07f5e66491 Merge pull request #519 from NateWright/main
Add maskable icons for Android PWA
2024-06-18 08:25:49 -06:00
Hargata Softworks
fe633f3220 Merge pull request #542 from kapcake/patch-1
Update money regex to allow more than 6 figures on integer part
2024-06-18 08:25:00 -06:00
kapcake
af1090553f Update money regex to not allow unlimited figures
The regex now allows up to 8 groups of 3 digits on the integer part, which is within the bounds of C# decimal, preventing the user to go out of bounds on the form input.
2024-06-18 15:14:26 +02:00
kapcake
92c2e66660 Update money regex to allow more than 6 figures on integer part 2024-06-18 15:02:20 +02:00
Nathan Wright
08372f9dcb Merge branch 'hargata:main' into main 2024-06-08 15:20:31 -04:00
Hargata Softworks
64ea0e2eee Merge pull request #533 from hargata/Hargata/532
add support for smtp clients that requires no authentication.
2024-05-31 09:11:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7ab476a88f add support for smtp clients that requires no authentication. 2024-05-31 09:11:09 -06:00
Hargata Softworks
de41ca911d Merge pull request #530 from hargata/Hargata/527
Minor Bug Fixes
2024-05-29 09:36:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dde9688f96 Updated supplies usage validation to allow for free supplies. 2024-05-28 14:54:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c1ca63edc0 removed unnecessary tostring 2024-05-28 14:26:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cbc430499f added new currency formatting function 2024-05-28 14:14:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4da9fa4802 See 514 2024-05-28 13:55:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
42586d9556 Updated version number 2024-05-28 12:56:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ea4387d4ab Add missing translation keys 2024-05-28 12:53:20 -06:00
Hargata Softworks
0707b515ab Merge pull request #525 from hargata/Hargata/sponsor.tiers
Updated Sponsors File Path
2024-05-21 15:42:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78ae71fc46 Updated Sponsors File Path 2024-05-21 15:41:05 -06:00
Hargata Softworks
3f62cd40e7 Merge pull request #524 from hargata/Hargata/sponsor.tiers
Add Sponsor Section
2024-05-21 15:32:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
47657c0093 Added sponsor section. 2024-05-21 15:29:46 -06:00
Hargata Softworks
a5b0fde4b6 Merge pull request #522 from hargata/Hargata/swal.uncensored
Hargata/swal.uncensored
2024-05-20 12:26:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78cc0b34b1 Updated version number 2024-05-20 12:25:07 -06:00
Hargata Softworks
e3017e986b Merge pull request #520 from hargata/Hargata/license.change
Removed commercial license restrictions from license.
2024-05-20 12:22:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e12cd876db removed unused files 2024-05-20 12:21:54 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5292e4b814 utilize uncensored version of sweet alert 2024-05-20 12:16:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dbdd16ab89 Removed commercial license restrictions from license. 2024-05-14 08:46:47 -06:00
nwright
61c2600286 added maskable icons 2024-05-13 19:34:03 -04:00
snpaul22
163a33ae3a Create init.sql
Script checks for "app" schema in Postgres during startup. It will create the schema automatically if it doesn't exist or skip if it does exist.
2024-05-04 11:27:50 -04:00
snpaul22
37d064aa62 Update docker-compose.postgresql.yml
Added Postgres volume bind for initialization script
2024-05-04 11:23:55 -04:00
Hargata Softworks
ddc3c2e1b5 Merge pull request #503 from hargata/Hargata/null.mail.config
null check for mail config
2024-04-25 12:45:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
49184b287b null check for mail config 2024-04-25 12:45:21 -06:00
Hargata Softworks
f6139bda0d Merge pull request #502 from hargata/Hargata/mailkit.upgrade
fix inefficiencies
2024-04-25 11:33:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a6471b823b fix inefficiencies 2024-04-25 11:11:54 -06:00
Hargata Softworks
7c34003647 Merge pull request #501 from hargata/Hargata/mailkit.upgrade
MailKit Upgrade
2024-04-25 10:46:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1aa21f9980 Uprgade from .NET SMTPClient to MailKit as the default smtpclient does not support modern protocols. 2024-04-25 10:45:55 -06:00
Hargata Softworks
ce4ca50939 Merge pull request #499 from hargata/Hargata/persist.metric.bug
fix getyear method
2024-04-23 23:50:31 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fb28260c4a fix getyear method 2024-04-23 23:50:06 -06:00
Hargata Softworks
626a904747 Merge pull request #498 from hargata/Hargata/persist.metric.bug
Minor bug fix
2024-04-23 23:43:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
893cdafdc5 Fixes a very minor bug where the persisted year metric blanks out when viewing a different car that doesn't have that specific year. 2024-04-23 23:42:09 -06:00
Hargata Softworks
dbfb7d7d9c Merge pull request #493 from hargata/Hargata/persist.dashboard
check against null instead of undefined,
2024-04-15 08:42:43 -06:00
DESKTOP-GENO133\IvanPlex
a66538a7db check against null instead of undefined, 2024-04-15 08:41:53 -06:00
Hargata Softworks
2f77d87d4f Merge pull request #492 from hargata/Hargata/persist.dashboard
temporarily persist dashboard metrics in sessionStorage
2024-04-15 08:25:54 -06:00
DESKTOP-GENO133\IvanPlex
de85ba984c temporarily persist dashboard metrics in sessionStorage 2024-04-15 08:20:37 -06:00
Hargata Softworks
caac1a05ae Merge pull request #480 from hargata/Hargata/fix.link.color
fix donation link colors
2024-04-12 08:04:44 -06:00
Hargata Softworks
eb5793b819 Merge pull request #490 from hargata/Hargata/fix.alt.fuel
Fix Alternate Fuel Mileage Bug
2024-04-12 08:04:28 -06:00
DESKTOP-GENO133\IvanPlex
5ef3e1e2ce updated version number 2024-04-12 08:04:04 -06:00
DESKTOP-GENO133\IvanPlex
d8b459e5ee persist regional formatting when viewing record statistics. 2024-04-12 08:01:25 -06:00
DESKTOP-GENO133\IvanPlex
7b40d58aa1 fix alternate fuel mileage auto converting to NA decimal format bug. 2024-04-12 07:54:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
809e9b838e fix donation link colors 2024-04-09 21:29:19 -06:00
87 changed files with 1201 additions and 10710 deletions

1
.env
View File

@@ -2,7 +2,6 @@ LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8 LANG=en_US.UTF-8
MailConfig__EmailServer="" MailConfig__EmailServer=""
MailConfig__EmailFrom="" MailConfig__EmailFrom=""
MailConfig__UseSSL="false"
MailConfig__Port=587 MailConfig__Port=587
MailConfig__Username="" MailConfig__Username=""
MailConfig__Password="" MailConfig__Password=""

View File

@@ -13,7 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" /> <PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="Npgsql" Version="8.0.2" /> <PackageReference Include="MailKit" Version="4.5.0" />
<PackageReference Include="Npgsql" Version="8.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

@@ -21,6 +21,9 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess; private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
private readonly IPlanRecordDataAccess _planRecordDataAccess;
private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess;
private readonly IUserAccessDataAccess _userAccessDataAccess; private readonly IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess; private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper; private readonly IReminderHelper _reminderHelper;
@@ -42,6 +45,9 @@ namespace CarCareTracker.Controllers
IReminderRecordDataAccess reminderRecordDataAccess, IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess, IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess, IOdometerRecordDataAccess odometerRecordDataAccess,
ISupplyRecordDataAccess supplyRecordDataAccess,
IPlanRecordDataAccess planRecordDataAccess,
IPlanRecordTemplateDataAccess planRecordTemplateDataAccess,
IUserAccessDataAccess userAccessDataAccess, IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess, IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper, IMailHelper mailHelper,
@@ -60,6 +66,9 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess; _reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess; _upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess; _odometerRecordDataAccess = odometerRecordDataAccess;
_supplyRecordDataAccess = supplyRecordDataAccess;
_planRecordDataAccess = planRecordDataAccess;
_planRecordTemplateDataAccess = planRecordTemplateDataAccess;
_userAccessDataAccess = userAccessDataAccess; _userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess; _userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper; _mailHelper = mailHelper;
@@ -95,14 +104,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/servicerecords")] [Route("/api/vehicle/servicerecords")]
public IActionResult ServiceRecords(int vehicleId) public IActionResult ServiceRecords(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result); return Json(result);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpPost] [HttpPost]
[Route("/api/vehicle/servicerecords/add")] [Route("/api/vehicle/servicerecords/add")]
public IActionResult AddServiceRecord(int vehicleId, ServiceRecordExportModel input) public IActionResult AddServiceRecord(int vehicleId, GenericRecordExportModel input)
{ {
var response = new OperationResponse(); var response = new OperationResponse();
if (vehicleId == default) if (vehicleId == default)
@@ -131,7 +148,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer), Mileage = int.Parse(input.Odometer),
Description = input.Description, Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
}; };
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -163,14 +181,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/repairrecords")] [Route("/api/vehicle/repairrecords")]
public IActionResult RepairRecords(int vehicleId) public IActionResult RepairRecords(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId); var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result); return Json(result);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpPost] [HttpPost]
[Route("/api/vehicle/repairrecords/add")] [Route("/api/vehicle/repairrecords/add")]
public IActionResult AddRepairRecord(int vehicleId, ServiceRecordExportModel input) public IActionResult AddRepairRecord(int vehicleId, GenericRecordExportModel input)
{ {
var response = new OperationResponse(); var response = new OperationResponse();
if (vehicleId == default) if (vehicleId == default)
@@ -199,7 +225,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer), Mileage = int.Parse(input.Odometer),
Description = input.Description, Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
}; };
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -231,14 +258,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/upgraderecords")] [Route("/api/vehicle/upgraderecords")]
public IActionResult UpgradeRecords(int vehicleId) public IActionResult UpgradeRecords(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result); return Json(result);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpPost] [HttpPost]
[Route("/api/vehicle/upgraderecords/add")] [Route("/api/vehicle/upgraderecords/add")]
public IActionResult AddUpgradeRecord(int vehicleId, ServiceRecordExportModel input) public IActionResult AddUpgradeRecord(int vehicleId, GenericRecordExportModel input)
{ {
var response = new OperationResponse(); var response = new OperationResponse();
if (vehicleId == default) if (vehicleId == default)
@@ -267,7 +302,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer), Mileage = int.Parse(input.Odometer),
Description = input.Description, Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
}; };
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -299,7 +335,15 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/taxrecords")] [Route("/api/vehicle/taxrecords")]
public IActionResult TaxRecords(int vehicleId) public IActionResult TaxRecords(int vehicleId)
{ {
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields });
return Json(result); return Json(result);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
@@ -332,7 +376,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date), Date = DateTime.Parse(input.Date),
Description = input.Description, Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
}; };
_taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord); _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Tax Record via API - Description: {taxRecord.Description}"); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Tax Record via API - Description: {taxRecord.Description}");
@@ -353,6 +398,14 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/odometerrecords/latest")] [Route("/api/vehicle/odometerrecords/latest")]
public IActionResult LastOdometer(int vehicleId) public IActionResult LastOdometer(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var result = _vehicleLogic.GetMaxMileage(vehicleId); var result = _vehicleLogic.GetMaxMileage(vehicleId);
return Json(result); return Json(result);
} }
@@ -361,13 +414,21 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/odometerrecords")] [Route("/api/vehicle/odometerrecords")]
public IActionResult OdometerRecords(int vehicleId) public IActionResult OdometerRecords(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
//determine if conversion is needed. //determine if conversion is needed.
if (vehicleRecords.All(x => x.InitialMileage == default)) if (vehicleRecords.All(x => x.InitialMileage == default))
{ {
vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords); vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords);
} }
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes }); var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields });
return Json(result); return Json(result);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
@@ -399,7 +460,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date), Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()) : int.Parse(input.InitialOdometer), InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()) : int.Parse(input.InitialOdometer),
Mileage = int.Parse(input.Odometer) Mileage = int.Parse(input.Odometer),
ExtraFields = input.ExtraFields
}; };
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord); _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Odometer Record via API - Mileage: {odometerRecord.Mileage.ToString()}"); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Odometer Record via API - Mileage: {odometerRecord.Mileage.ToString()}");
@@ -419,6 +481,14 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/gasrecords")] [Route("/api/vehicle/gasrecords")]
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG) public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG) var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG)
.Select(x => new GasRecordExportModel { .Select(x => new GasRecordExportModel {
@@ -429,7 +499,8 @@ namespace CarCareTracker.Controllers
FuelEconomy = x.MilesPerGallon.ToString(), FuelEconomy = x.MilesPerGallon.ToString(),
IsFillToFull = x.IsFillToFull.ToString(), IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(), MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes Notes = x.Notes,
ExtraFields = x.ExtraFields
}); });
return Json(result); return Json(result);
} }
@@ -470,7 +541,8 @@ namespace CarCareTracker.Controllers
IsFillToFull = bool.Parse(input.IsFillToFull), IsFillToFull = bool.Parse(input.IsFillToFull),
MissedFuelUp = bool.Parse(input.MissedFuelUp), MissedFuelUp = bool.Parse(input.MissedFuelUp),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
}; };
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord); _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -502,9 +574,17 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/reminders")] [Route("/api/vehicle/reminders")]
public IActionResult Reminders(int vehicleId) public IActionResult Reminders(int vehicleId)
{ {
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
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}); 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()});
return Json(results); return Json(results);
} }
[Authorize(Roles = nameof(UserData.IsRootUser))] [Authorize(Roles = nameof(UserData.IsRootUser))]
@@ -562,6 +642,49 @@ namespace CarCareTracker.Controllers
} }
[Authorize(Roles = nameof(UserData.IsRootUser))] [Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet] [HttpGet]
[Route("/api/cleanup")]
public IActionResult CleanUp(bool deepClean = false)
{
var jsonResponse = new Dictionary<string, string>();
//Clear out temp folder
var tempFilesDeleted = _fileHelper.ClearTempFolder();
jsonResponse.Add("temp_files_deleted", tempFilesDeleted.ToString());
if (deepClean)
{
//clear out unused vehicle thumbnails
var vehicles = _dataAccess.GetVehicles();
var vehicleImages = vehicles.Select(x => x.ImageLocation).Where(x => x.StartsWith("/images/")).Select(x=>Path.GetFileName(x)).ToList();
if (vehicleImages.Any())
{
var thumbnailsDeleted = _fileHelper.ClearUnlinkedThumbnails(vehicleImages);
jsonResponse.Add("unlinked_thumbnails_deleted", thumbnailsDeleted.ToString());
}
var vehicleDocuments = new List<string>();
foreach(Vehicle vehicle in vehicles)
{
vehicleDocuments.AddRange(_serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y=>Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_noteDataAccess.GetNotesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
}
//shop supplies
vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
if (vehicleDocuments.Any())
{
var documentsDeleted = _fileHelper.ClearUnlinkedDocuments(vehicleDocuments);
jsonResponse.Add("unlinked_documents_deleted", documentsDeleted.ToString());
}
}
return Json(jsonResponse);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/demo/restore")] [Route("/api/demo/restore")]
public IActionResult RestoreDemo() public IActionResult RestoreDemo()
{ {

View File

@@ -69,6 +69,7 @@ namespace CarCareTracker.Controllers
LicensePlate = x.LicensePlate, LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate, SoldDate = x.SoldDate,
IsElectric = x.IsElectric, IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours, UseHours = x.UseHours,
ExtraFields = x.ExtraFields, ExtraFields = x.ExtraFields,
Tags = x.Tags, Tags = x.Tags,
@@ -105,7 +106,7 @@ namespace CarCareTracker.Controllers
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, 0, DateTime.Now).FirstOrDefault(); var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, 0, DateTime.Now).FirstOrDefault();
return PartialView("_ReminderRecordCalendarModal", reminderUrgency); return PartialView("_ReminderRecordCalendarModal", reminderUrgency);
} }
public IActionResult Settings() public async Task<IActionResult> Settings()
{ {
var userConfig = _config.GetUserConfig(User); var userConfig = _config.GetUserConfig(User);
var languages = _fileHelper.GetLanguages(); var languages = _fileHelper.GetLanguages();
@@ -114,6 +115,16 @@ namespace CarCareTracker.Controllers
UserConfig = userConfig, UserConfig = userConfig,
UILanguages = languages UILanguages = languages
}; };
try
{
var httpClient = new HttpClient();
var sponsorsData = await httpClient.GetFromJsonAsync<Sponsors>(StaticHelper.SponsorsPath) ?? new Sponsors();
viewModel.Sponsors = sponsorsData;
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve sponsors: {ex.Message}");
}
return PartialView("_Settings", viewModel); return PartialView("_Settings", viewModel);
} }
[HttpPost] [HttpPost]
@@ -209,6 +220,13 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name; var userName = User.Identity.Name;
return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName }); return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName });
} }
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetRootAccountInformationModal()
{
var userName = User.Identity.Name;
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() public IActionResult Error()
{ {

View File

@@ -34,10 +34,16 @@ namespace CarCareTracker.Controllers
{ {
var generatedState = Guid.NewGuid().ToString().Substring(0, 8); var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
remoteAuthConfig.State = generatedState; remoteAuthConfig.State = generatedState;
var pkceKeyPair = _loginLogic.GetPKCEChallengeCode();
remoteAuthConfig.CodeChallenge = pkceKeyPair.Value;
if (remoteAuthConfig.ValidateState) if (remoteAuthConfig.ValidateState)
{ {
Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
} }
if (remoteAuthConfig.UsePKCE)
{
Response.Cookies.Append("OIDC_VERIFIER", pkceKeyPair.Key, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
}
var remoteAuthURL = remoteAuthConfig.RemoteAuthURL; var remoteAuthURL = remoteAuthConfig.RemoteAuthURL;
return Redirect(remoteAuthURL); return Redirect(remoteAuthURL);
} }
@@ -60,10 +66,16 @@ namespace CarCareTracker.Controllers
var remoteAuthConfig = _config.GetOpenIDConfig(); var remoteAuthConfig = _config.GetOpenIDConfig();
var generatedState = Guid.NewGuid().ToString().Substring(0, 8); var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
remoteAuthConfig.State = generatedState; remoteAuthConfig.State = generatedState;
var pkceKeyPair = _loginLogic.GetPKCEChallengeCode();
remoteAuthConfig.CodeChallenge = pkceKeyPair.Value;
if (remoteAuthConfig.ValidateState) if (remoteAuthConfig.ValidateState)
{ {
Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
} }
if (remoteAuthConfig.UsePKCE)
{
Response.Cookies.Append("OIDC_VERIFIER", pkceKeyPair.Key, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
}
var remoteAuthURL = remoteAuthConfig.RemoteAuthURL; var remoteAuthURL = remoteAuthConfig.RemoteAuthURL;
return Json(remoteAuthURL); return Json(remoteAuthURL);
} }
@@ -99,6 +111,16 @@ namespace CarCareTracker.Controllers
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret), new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL) 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) var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
{ {
Content = new FormUrlEncodedContent(httpParams) Content = new FormUrlEncodedContent(httpParams)
@@ -137,6 +159,11 @@ namespace CarCareTracker.Controllers
} else } else
{ {
_logger.LogInformation("OpenID Provider did not provide a valid id_token"); _logger.LogInformation("OpenID Provider did not provide a valid id_token");
if (!string.IsNullOrWhiteSpace(tokenResult))
{
//if something was returned from the IdP but it's invalid, we want to log it as an error.
_logger.LogError($"Expected id_token, received {tokenResult}");
}
} }
} else } else
{ {
@@ -220,7 +247,7 @@ namespace CarCareTracker.Controllers
var result = _loginLogic.ResetPasswordByUser(credentials); var result = _loginLogic.ResetPasswordByUser(credentials);
return Json(result); return Json(result);
} }
[Authorize] //User must already be logged in to do this. [Authorize(Roles = nameof(UserData.IsRootUser))] //User must already be logged in as root user to do this.
[HttpPost] [HttpPost]
public IActionResult CreateLoginCreds(LoginModel credentials) public IActionResult CreateLoginCreds(LoginModel credentials)
{ {
@@ -235,7 +262,7 @@ namespace CarCareTracker.Controllers
} }
return Json(false); return Json(false);
} }
[Authorize] [Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost] [HttpPost]
public IActionResult DestroyLoginCreds() public IActionResult DestroyLoginCreds()
{ {

View File

@@ -219,37 +219,53 @@ namespace CarCareTracker.Controllers
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory); string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
if (mode == ImportMode.ServiceRecord) if (mode == ImportMode.ServiceRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) }); var exportData = vehicleRecords.Select(x => new GenericRecordExportModel {
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); //custom writer
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
} }
writer.Dispose();
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
} }
} }
else if (mode == ImportMode.RepairRecord) else if (mode == ImportMode.RepairRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId); var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) }); var exportData = vehicleRecords.Select(x => new GenericRecordExportModel {
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteGenericRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -257,17 +273,23 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.UpgradeRecord) else if (mode == ImportMode.UpgradeRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) }); var exportData = vehicleRecords.Select(x => new GenericRecordExportModel {
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteGenericRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -275,17 +297,22 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.OdometerRecord) else if (mode == ImportMode.OdometerRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Notes = x.Notes, InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) }); var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel {
Date = x.Date.ToShortDateString(),
Notes = x.Notes,
InitialOdometer = x.InitialMileage.ToString(),
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -293,8 +320,6 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.SupplyRecord) else if (mode == ImportMode.SupplyRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId); var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
@@ -307,13 +332,14 @@ namespace CarCareTracker.Controllers
PartQuantity = x.Quantity.ToString(), PartQuantity = x.Quantity.ToString(),
PartSupplier = x.PartSupplier, PartSupplier = x.PartSupplier,
Notes = x.Notes, Notes = x.Notes,
Tags = string.Join(" ", x.Tags) Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
}); });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -321,17 +347,22 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.TaxRecord) else if (mode == ImportMode.TaxRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Tags = string.Join(" ", x.Tags) }); var exportData = vehicleRecords.Select(x => new TaxRecordExportModel {
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteTaxRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -339,8 +370,6 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.PlanRecord) else if (mode == ImportMode.PlanRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
@@ -353,13 +382,14 @@ namespace CarCareTracker.Controllers
Type = x.ImportMode.ToString(), Type = x.ImportMode.ToString(),
Priority = x.Priority.ToString(), Priority = x.Priority.ToString(),
Progress = x.Progress.ToString(), Progress = x.Progress.ToString(),
Notes = x.Notes Notes = x.Notes,
ExtraFields = x.ExtraFields
}); });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WritePlanRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -367,8 +397,6 @@ namespace CarCareTracker.Controllers
} }
else if (mode == ImportMode.GasRecord) else if (mode == ImportMode.GasRecord)
{ {
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG; bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG; bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
@@ -383,13 +411,14 @@ namespace CarCareTracker.Controllers
IsFillToFull = x.IsFillToFull.ToString(), IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(), MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes, Notes = x.Notes,
Tags = string.Join(" ", x.Tags) Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
}); });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{ {
csv.WriteRecords(exportData); StaticHelper.WriteGasRecordExportModel(csv, exportData);
} }
} }
return Json($"/{fileNameToExport}"); return Json($"/{fileNameToExport}");
@@ -417,7 +446,7 @@ namespace CarCareTracker.Controllers
{ {
using (var reader = new StreamReader(fullFileName)) using (var reader = new StreamReader(fullFileName))
{ {
var config = new CsvHelper.Configuration.CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture); var config = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture);
config.MissingFieldFound = null; config.MissingFieldFound = null;
config.HeaderValidated = null; config.HeaderValidated = null;
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); }; config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
@@ -427,6 +456,7 @@ namespace CarCareTracker.Controllers
var records = csv.GetRecords<ImportModel>().ToList(); var records = csv.GetRecords<ImportModel>().ToList();
if (records.Any()) if (records.Any())
{ {
var requiredExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)mode).ExtraFields.Where(x=>x.IsRequired).Select(y=>y.Name);
foreach (ImportModel importModel in records) foreach (ImportModel importModel in records)
{ {
if (mode == ImportMode.GasRecord) if (mode == ImportMode.GasRecord)
@@ -439,7 +469,8 @@ namespace CarCareTracker.Controllers
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any), Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price)) if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{ {
@@ -495,7 +526,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -518,7 +550,8 @@ namespace CarCareTracker.Controllers
InitialMileage = string.IsNullOrWhiteSpace(importModel.InitialOdometer) ? 0 : decimal.ToInt32(decimal.Parse(importModel.InitialOdometer, NumberStyles.Any)), InitialMileage = string.IsNullOrWhiteSpace(importModel.InitialOdometer) ? 0 : decimal.ToInt32(decimal.Parse(importModel.InitialOdometer, NumberStyles.Any)),
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord); _odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
} }
@@ -537,7 +570,8 @@ namespace CarCareTracker.Controllers
Priority = parsedPriority, Priority = parsedPriority,
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any) Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord); _planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord);
} }
@@ -551,7 +585,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -575,7 +610,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert) if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -601,7 +637,8 @@ namespace CarCareTracker.Controllers
Description = importModel.Description, Description = importModel.Description,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Notes = importModel.Notes, Notes = importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord); _supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord);
} }
@@ -614,7 +651,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList() Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
}; };
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord); _taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
} }
@@ -1153,6 +1191,10 @@ namespace CarCareTracker.Controllers
{ {
numbersArray.Add(upgradeRecords.Min(x => x.Date.Year)); numbersArray.Add(upgradeRecords.Min(x => x.Date.Year));
} }
if (odometerRecords.Any())
{
numbersArray.Add(odometerRecords.Min(x => x.Date.Year));
}
var minYear = numbersArray.Any() ? numbersArray.Min() : DateTime.Now.AddYears(-5).Year; var minYear = numbersArray.Any() ? numbersArray.Min() : DateTime.Now.AddYears(-5).Year;
var yearDifference = DateTime.Now.Year - minYear + 1; var yearDifference = DateTime.Now.Year - minYear + 1;
for (int i = 0; i < yearDifference; i++) for (int i = 0; i < yearDifference; i++)
@@ -1783,6 +1825,7 @@ namespace CarCareTracker.Controllers
[HttpPost] [HttpPost]
public IActionResult SaveNoteToVehicleId(Note note) public IActionResult SaveNoteToVehicleId(Note note)
{ {
note.Files = note.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _noteDataAccess.SaveNoteToVehicle(note); var result = _noteDataAccess.SaveNoteToVehicle(note);
if (result) if (result)
{ {
@@ -2379,6 +2422,12 @@ namespace CarCareTracker.Controllers
#endregion #endregion
#region "Shared Methods" #region "Shared Methods"
[HttpPost] [HttpPost]
public IActionResult GetFilesPendingUpload(List<UploadedFiles> uploadedFiles)
{
var filesPendingUpload = uploadedFiles.Where(x => x.Location.StartsWith("/temp/")).ToList();
return PartialView("_FilesToUpload", filesPendingUpload);
}
[HttpPost]
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
public IActionResult SearchRecords(int vehicleId, string searchQuery) public IActionResult SearchRecords(int vehicleId, string searchQuery)
{ {

View File

@@ -147,7 +147,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -141,7 +141,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} catch (Exception ex) } catch (Exception ex)
{ {

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -146,7 +146,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", vehicleId); ctext.Parameters.AddWithValue("id", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -176,7 +176,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("vehicleId", vehicleId); ctext.Parameters.AddWithValue("vehicleId", vehicleId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -198,7 +199,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("userId", userId); ctext.Parameters.AddWithValue("userId", userId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -94,7 +94,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd)) using (var ctext = pgDataSource.CreateCommand(cmd))
{ {
ctext.Parameters.AddWithValue("id", userId); ctext.Parameters.AddWithValue("id", userId);
return ctext.ExecuteNonQuery() > 0; ctext.ExecuteNonQuery();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -13,6 +13,9 @@ namespace CarCareTracker.Helper
bool RestoreBackup(string fileName, bool clearExisting = false); bool RestoreBackup(string fileName, bool clearExisting = false);
string MakeAttachmentsExport(List<GenericReportModel> exportData); string MakeAttachmentsExport(List<GenericReportModel> exportData);
List<string> GetLanguages(); List<string> GetLanguages();
int ClearTempFolder();
int ClearUnlinkedThumbnails(List<string> linkedImages);
int ClearUnlinkedDocuments(List<string> linkedDocuments);
} }
public class FileHelper : IFileHelper public class FileHelper : IFileHelper
{ {
@@ -314,5 +317,56 @@ namespace CarCareTracker.Helper
return false; return false;
} }
} }
public int ClearTempFolder()
{
int filesDeleted = 0;
var tempPath = GetFullFilePath("temp", false);
if (Directory.Exists(tempPath))
{
var files = Directory.GetFiles(tempPath);
foreach (var file in files)
{
File.Delete(file);
filesDeleted++;
}
}
return filesDeleted;
}
public int ClearUnlinkedThumbnails(List<string> linkedImages)
{
int filesDeleted = 0;
var imagePath = GetFullFilePath("images", false);
if (Directory.Exists(imagePath))
{
var files = Directory.GetFiles(imagePath);
foreach(var file in files)
{
if (!linkedImages.Contains(Path.GetFileName(file)))
{
File.Delete(file);
filesDeleted++;
}
}
}
return filesDeleted;
}
public int ClearUnlinkedDocuments(List<string> linkedDocuments)
{
int filesDeleted = 0;
var documentPath = GetFullFilePath("documents", false);
if (Directory.Exists(documentPath))
{
var files = Directory.GetFiles(documentPath);
foreach (var file in files)
{
if (!linkedDocuments.Contains(Path.GetFileName(file)))
{
File.Delete(file);
filesDeleted++;
}
}
}
return filesDeleted;
}
} }
} }

View File

@@ -1,6 +1,6 @@
using CarCareTracker.Models; using CarCareTracker.Models;
using System.Net.Mail; using MimeKit;
using System.Net; using MailKit.Net.Smtp;
namespace CarCareTracker.Helper namespace CarCareTracker.Helper
{ {
@@ -15,13 +15,16 @@ namespace CarCareTracker.Helper
{ {
private readonly MailConfig mailConfig; private readonly MailConfig mailConfig;
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
private readonly ILogger<MailHelper> _logger;
public MailHelper( public MailHelper(
IConfiguration config, IConfiguration config,
IFileHelper fileHelper IFileHelper fileHelper,
ILogger<MailHelper> logger
) { ) {
//load mailConfig from Configuration //load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>(); mailConfig = config.GetSection("MailConfig").Get<MailConfig>() ?? new MailConfig();
_fileHelper = fileHelper; _fileHelper = fileHelper;
_logger = logger;
} }
public OperationResponse NotifyUserForRegistration(string emailAddress, string token) public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{ {
@@ -34,7 +37,7 @@ namespace CarCareTracker.Helper
} }
string emailSubject = "Your Registration Token for LubeLogger"; string emailSubject = "Your Registration Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}"; string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {
return new OperationResponse { Success = true, Message = "Email Sent!" }; return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -55,7 +58,7 @@ namespace CarCareTracker.Helper
} }
string emailSubject = "Your Password Reset Token for LubeLogger"; string emailSubject = "Your Password Reset Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}"; string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {
return new OperationResponse { Success = true, Message = "Email Sent!" }; return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -77,7 +80,7 @@ namespace CarCareTracker.Helper
} }
string emailSubject = "Your User Account Update Token for LubeLogger"; string emailSubject = "Your User Account Update Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please update your account for LubeLogger using the token: {token}"; string emailBody = $"A token has been generated on your behalf, please update your account for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody); var result = SendEmail(new List<string> { emailAddress}, emailSubject, emailBody);
if (result) if (result)
{ {
return new OperationResponse { Success = true, Message = "Email Sent!" }; return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -116,43 +119,48 @@ namespace CarCareTracker.Helper
emailBody = emailBody.Replace("{TableBody}", tableBody); emailBody = emailBody.Replace("{TableBody}", tableBody);
try try
{ {
foreach (string emailAddress in emailAddresses) SendEmail(emailAddresses, emailSubject, emailBody);
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
}
return new OperationResponse { Success = true, Message = "Email Sent!" }; return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex) } catch (Exception ex)
{ {
return new OperationResponse { Success = false, Message = ex.Message }; return new OperationResponse { Success = false, Message = ex.Message };
} }
} }
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) { private bool SendEmail(List<string> emailTo, string emailSubject, string emailBody) {
string to = emailTo;
string from = mailConfig.EmailFrom; string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer; var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to); var message = new MimeMessage();
message.Subject = emailSubject; message.From.Add(new MailboxAddress(from, from));
message.Body = emailBody; foreach(string emailRecipient in emailTo)
message.IsBodyHtml = isBodyHtml;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
{ {
if (useAsync) message.To.Add(new MailboxAddress(emailRecipient, emailRecipient));
{ }
client.SendMailAsync(message, new CancellationToken()); message.Subject = emailSubject;
var builder = new BodyBuilder();
builder.HtmlBody = emailBody;
message.Body = builder.ToMessageBody();
using (var client = new SmtpClient())
{
client.Connect(server, mailConfig.Port, MailKit.Security.SecureSocketOptions.Auto);
//perform authentication if either username or password is provided.
//do not perform authentication if neither are provided.
if (!string.IsNullOrWhiteSpace(mailConfig.Username) || !string.IsNullOrWhiteSpace(mailConfig.Password)) {
client.Authenticate(mailConfig.Username, mailConfig.Password);
} }
else try
{ {
client.Send(message); client.Send(message);
client.Disconnect(true);
return true;
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
} }
return true;
}
catch (Exception ex)
{
return false;
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models; using CarCareTracker.Models;
using CsvHelper;
using System.Globalization; using System.Globalization;
namespace CarCareTracker.Helper namespace CarCareTracker.Helper
@@ -8,13 +9,13 @@ namespace CarCareTracker.Helper
/// </summary> /// </summary>
public static class StaticHelper public static class StaticHelper
{ {
public static string VersionNumber = "1.3.0"; public static string VersionNumber = "1.3.6";
public static string DbName = "data/cartracker.db"; public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json"; public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later"; public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt"; public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx"; public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public static string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input) public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
{ {
switch (input) switch (input)
@@ -287,5 +288,202 @@ namespace CarCareTracker.Helper
return "bi-file-bar-graph"; return "bi-file-bar-graph";
} }
} }
//CSV Write Methods
public static void WriteGenericRecordExportModel(CsvWriter _csv, IEnumerable<GenericRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(GenericRecordExportModel.Date));
_csv.WriteField(nameof(GenericRecordExportModel.Description));
_csv.WriteField(nameof(GenericRecordExportModel.Cost));
_csv.WriteField(nameof(GenericRecordExportModel.Notes));
_csv.WriteField(nameof(GenericRecordExportModel.Odometer));
_csv.WriteField(nameof(GenericRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (GenericRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteOdometerRecordExportModel(CsvWriter _csv, IEnumerable<OdometerRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(OdometerRecordExportModel.Date));
_csv.WriteField(nameof(OdometerRecordExportModel.InitialOdometer));
_csv.WriteField(nameof(OdometerRecordExportModel.Odometer));
_csv.WriteField(nameof(OdometerRecordExportModel.Notes));
_csv.WriteField(nameof(OdometerRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (OdometerRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.InitialOdometer);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteTaxRecordExportModel(CsvWriter _csv, IEnumerable<TaxRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(TaxRecordExportModel.Date));
_csv.WriteField(nameof(TaxRecordExportModel.Description));
_csv.WriteField(nameof(TaxRecordExportModel.Cost));
_csv.WriteField(nameof(TaxRecordExportModel.Notes));
_csv.WriteField(nameof(TaxRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (TaxRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteSupplyRecordExportModel(CsvWriter _csv, IEnumerable<SupplyRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(SupplyRecordExportModel.Date));
_csv.WriteField(nameof(SupplyRecordExportModel.PartNumber));
_csv.WriteField(nameof(SupplyRecordExportModel.PartSupplier));
_csv.WriteField(nameof(SupplyRecordExportModel.PartQuantity));
_csv.WriteField(nameof(SupplyRecordExportModel.Description));
_csv.WriteField(nameof(SupplyRecordExportModel.Notes));
_csv.WriteField(nameof(SupplyRecordExportModel.Cost));
_csv.WriteField(nameof(SupplyRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (SupplyRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.PartNumber);
_csv.WriteField(genericRecord.PartSupplier);
_csv.WriteField(genericRecord.PartQuantity);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WritePlanRecordExportModel(CsvWriter _csv, IEnumerable<PlanRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(PlanRecordExportModel.DateCreated));
_csv.WriteField(nameof(PlanRecordExportModel.DateModified));
_csv.WriteField(nameof(PlanRecordExportModel.Description));
_csv.WriteField(nameof(PlanRecordExportModel.Notes));
_csv.WriteField(nameof(PlanRecordExportModel.Type));
_csv.WriteField(nameof(PlanRecordExportModel.Priority));
_csv.WriteField(nameof(PlanRecordExportModel.Progress));
_csv.WriteField(nameof(PlanRecordExportModel.Cost));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (PlanRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.DateCreated);
_csv.WriteField(genericRecord.DateModified);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Type);
_csv.WriteField(genericRecord.Priority);
_csv.WriteField(genericRecord.Progress);
_csv.WriteField(genericRecord.Cost);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteGasRecordExportModel(CsvWriter _csv, IEnumerable<GasRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(GasRecordExportModel.Date));
_csv.WriteField(nameof(GasRecordExportModel.Odometer));
_csv.WriteField(nameof(GasRecordExportModel.FuelConsumed));
_csv.WriteField(nameof(GasRecordExportModel.Cost));
_csv.WriteField(nameof(GasRecordExportModel.FuelEconomy));
_csv.WriteField(nameof(GasRecordExportModel.IsFillToFull));
_csv.WriteField(nameof(GasRecordExportModel.MissedFuelUp));
_csv.WriteField(nameof(GasRecordExportModel.Notes));
_csv.WriteField(nameof(GasRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (GasRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.FuelConsumed);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.FuelEconomy);
_csv.WriteField(genericRecord.IsFillToFull);
_csv.WriteField(genericRecord.MissedFuelUp);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
} }
} }

View File

@@ -1,11 +1,6 @@
LubeLogger by Hargata Softworks is licensed under the MIT License for individual
and personal use. Commercial users and/or corporate entities are required
to maintain an active subscription in order to continue using LubeLogger.
For pricing information please contact us at hargatasoftworks@gmail.com
MIT License MIT License
Copyright (c) 2023 Hargata Softworks Copyright (c) 2024 Hargata Softworks
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -2,6 +2,7 @@
using CarCareTracker.Helper; using CarCareTracker.Helper;
using CarCareTracker.Models; using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -28,7 +29,7 @@ namespace CarCareTracker.Logic
bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset); bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset);
List<UserData> GetAllUsers(); List<UserData> GetAllUsers();
List<Token> GetAllTokens(); List<Token> GetAllTokens();
KeyValuePair<string, string> GetPKCEChallengeCode();
} }
public class LoginLogic : ILoginLogic public class LoginLogic : ILoginLogic
{ {
@@ -439,6 +440,14 @@ namespace CarCareTracker.Logic
{ {
return Guid.NewGuid().ToString().Substring(0, 8); return Guid.NewGuid().ToString().Substring(0, 8);
} }
public KeyValuePair<string, string> GetPKCEChallengeCode()
{
var verifierCode = Base64UrlEncoder.Encode(Guid.NewGuid().ToString().Replace("-", ""));
var verifierBytes = Encoding.UTF8.GetBytes(verifierCode);
var hashedCode = SHA256.Create().ComputeHash(verifierBytes);
var encodedChallengeCode = Base64UrlEncoder.Encode(hashedCode);
return new KeyValuePair<string, string>(verifierCode, encodedChallengeCode);
}
public bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset) public bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset)
{ {
bool result = false; bool result = false;

View File

@@ -27,6 +27,18 @@ namespace CarCareTracker.MapProfile
Map(m => m.Type).Name(["type"]); Map(m => m.Type).Name(["type"]);
Map(m => m.Priority).Name(["priority"]); Map(m => m.Priority).Name(["priority"]);
Map(m => m.Tags).Name(["tags"]); Map(m => m.Tags).Name(["tags"]);
Map(m => m.ExtraFields).Convert(row =>
{
var attributes = new Dictionary<string, string>();
foreach (var header in row.Row.HeaderRecord)
{
if (header.ToLower().StartsWith("extrafield_"))
{
attributes.Add(header.Substring(11), row.Row.GetField(header));
}
}
return attributes;
});
} }
} }
} }

View File

@@ -4,7 +4,6 @@
{ {
public string EmailServer { get; set; } public string EmailServer { get; set; }
public string EmailFrom { get; set; } public string EmailFrom { get; set; }
public bool UseSSL { get; set; }
public int Port { get; set; } public int Port { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }

View File

@@ -10,9 +10,18 @@
public string RedirectURL { get; set; } public string RedirectURL { get; set; }
public string Scope { get; set; } public string Scope { get; set; }
public string State { get; set; } public string State { 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 string LogOutURL { get; set; } = ""; public string LogOutURL { get; set; } = "";
public string RemoteAuthURL { get { return $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; } } public string RemoteAuthURL { get {
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
if (UsePKCE)
{
redirectUrl += $"&code_challenge={CodeChallenge}&code_challenge_method=S256";
}
return redirectUrl;
} }
} }
} }

View File

@@ -4,5 +4,6 @@ namespace CarCareTracker.Models
{ {
public UserConfig UserConfig { get; set; } public UserConfig UserConfig { get; set; }
public List<string> UILanguages { get; set; } public List<string> UILanguages { get; set; }
public Sponsors Sponsors { get; set; } = new Sponsors();
} }
} }

View File

@@ -25,6 +25,7 @@
public string PartSupplier { get; set; } public string PartSupplier { get; set; }
public string PartQuantity { get; set; } public string PartQuantity { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public Dictionary<string,string> ExtraFields {get;set;}
} }
public class SupplyRecordExportModel public class SupplyRecordExportModel
@@ -37,9 +38,9 @@
public string Cost { get; set; } public string Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
public class GenericRecordExportModel
public class ServiceRecordExportModel
{ {
public string Date { get; set; } public string Date { get; set; }
public string Odometer { get; set; } public string Odometer { get; set; }
@@ -47,6 +48,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
public class OdometerRecordExportModel public class OdometerRecordExportModel
{ {
@@ -55,6 +57,7 @@
public string Odometer { get; set; } public string Odometer { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
public class TaxRecordExportModel public class TaxRecordExportModel
{ {
@@ -63,6 +66,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
public class GasRecordExportModel public class GasRecordExportModel
{ {
@@ -75,6 +79,7 @@
public string MissedFuelUp { get; set; } public string MissedFuelUp { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
public class ReminderExportModel public class ReminderExportModel
{ {
@@ -82,6 +87,8 @@
public string Urgency { get; set; } public string Urgency { get; set; }
public string Metric { get; set; } public string Metric { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string DueDate { get; set; }
public string DueOdometer { get; set; }
} }
public class PlanRecordExportModel public class PlanRecordExportModel
{ {
@@ -93,6 +100,6 @@
public string Priority { get; set; } public string Priority { get; set; }
public string Progress { get; set; } public string Progress { get; set; }
public string Cost { get; set; } public string Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; }
} }
} }

10
Models/Sponsors.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public class Sponsors
{
public List<string> LifeTime { get; set; } = new List<string>();
public List<string> Bronze { get; set; } = new List<string>();
public List<string> Silver { get; set; } = new List<string>();
public List<string> Gold { get; set; } = new List<string>();
}
}

View File

@@ -13,6 +13,7 @@
public decimal PurchasePrice { get; set; } public decimal PurchasePrice { get; set; }
public decimal SoldPrice { get; set; } public decimal SoldPrice { get; set; }
public bool IsElectric { get; set; } = false; public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false; public bool UseHours { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>(); public List<string> Tags { get; set; } = new List<string>();

View File

@@ -10,6 +10,7 @@
public string LicensePlate { get; set; } public string LicensePlate { get; set; }
public string SoldDate { get; set; } public string SoldDate { get; set; }
public bool IsElectric { get; set; } = false; public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false; public bool UseHours { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>(); public List<string> Tags { get; set; } = new List<string>();

View File

@@ -22,6 +22,9 @@ LubeLogger is available as both a Docker Image and a Windows Standalone Executab
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
### Kubernetes Deployment
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
### Need Help? ### Need Help?
[Documentation](https://docs.lubelogger.com/) [Documentation](https://docs.lubelogger.com/)
@@ -30,19 +33,18 @@ Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started)
[Search Existing Issues](https://github.com/hargata/lubelog/issues) [Search Existing Issues](https://github.com/hargata/lubelog/issues)
## Dependencies ## Dependencies
- Bootstrap - [Bootstrap](https://github.com/twbs/bootstrap)
- LiteDB - [LiteDB](https://github.com/mbdavid/litedb)
- Npgsql - [Npgsql](https://github.com/npgsql/npgsql)
- Bootstrap-DatePicker - [Bootstrap-DatePicker](https://github.com/uxsolutions/bootstrap-datepicker)
- SweetAlert2 - [SweetAlert2](https://github.com/sweetalert2/sweetalert2)
- CsvHelper - [CsvHelper](https://github.com/JoshClose/CsvHelper)
- Chart.js - [Chart.js](https://github.com/chartjs/Chart.js)
- Drawdown - [Drawdown](https://github.com/adamvleggett/drawdown)
- [MailKit](https://github.com/jstedfast/MailKit)
## License ## License
LubeLogger utilizes a dual-licensing model, see [License](/LICENSE) for more information MIT
## Support ## Support
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144) Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
Note: Commercial users are required to maintain an active Patreon subscripton to be compliant with our licensing model.

View File

@@ -40,6 +40,55 @@
No Params No Params
</div> </div>
</div> </div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords</code>
</div>
<div class="col-3">
Returns a list of odometer records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/latest</code>
</div>
<div class="col-3">
Returns last reported odometer for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/add</code>
</div>
<div class="col-3">
Adds Odometer Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row"> <div class="row">
<div class="col-1"> <div class="col-1">
GET GET
@@ -270,56 +319,22 @@
No Params(must be root user) No Params(must be root user)
</div> </div>
</div> </div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/cleanup</code>
</div>
<div class="col-3">
Clears out temp files. Deep clean will also delete unlinked thumbnails and documents. Returns number of deleted files.
</div>
<div class="col-3">
(must be root user)<br />
deepClean(bool) - Perform deep clean
</div>
</div>
} }
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords</code>
</div>
<div class="col-3">
Returns a list of odometer records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/latest</code>
</div>
<div class="col-3">
Returns last reported odometer for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/add</code>
</div>
<div class="col-3">
Adds Odometer Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
}
</div>
</div>
<script> <script>
$('.copyable').on('click', function (e) { $('.copyable').on('click', function (e) {
copyToClipboard(e.currentTarget); copyToClipboard(e.currentTarget);

View File

@@ -41,7 +41,12 @@
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</span></a> <a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</span></a>
</li> </li>
} }
@if (!User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<li>
<button class="nav-link" onclick="showRootAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button>
</li>
} else
{ {
<li> <li>
<button class="nav-link" onclick="showAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button> <button class="nav-link" onclick="showAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button>
@@ -90,7 +95,12 @@
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</a> <a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</a>
</li> </li>
} }
@if (!User.IsInRole(nameof(UserData.IsRootUser))) @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> <li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button> <button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>

View File

@@ -7,7 +7,7 @@
var userLanguage = userConfig.UserLanguage; var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="addVehicleModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5> <h5 class="modal-title" id="updateAccountModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@@ -0,0 +1,26 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model UserData
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="updateRootAccountModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="form-inline">
<div class="form-group">
<label for="inputUsername">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Account Username")" value="@Model.UserName">
<label for="inputPassword">@translator.Translate(userLanguage, "Password")</label>
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "Password")" value="">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideAccountInformationModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="validateAndSaveRootUserAccount()" class="btn btn-primary">@translator.Translate(userLanguage, "Update")</button>
</div>

View File

@@ -219,7 +219,7 @@
</p> </p>
<p class="lead"> <p class="lead">
If you enjoyed using this app, please consider spreading the good word.<br /> If you enjoyed using this app, please consider spreading the good word.<br />
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a> If you want to support the development of this project, consider subscribing to <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
</p> </p>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Hometown Shoutout</h6> <h6 class="display-7 mt-2">Hometown Shoutout</h6>
@@ -247,9 +247,11 @@
<li class="list-group-item">CsvHelper</li> <li class="list-group-item">CsvHelper</li>
<li class="list-group-item">Chart.js</li> <li class="list-group-item">Chart.js</li>
<li class="list-group-item">Drawdown</li> <li class="list-group-item">Drawdown</li>
<li class="list-group-item">MailKit</li>
</ul> </ul>
</div> </div>
</div> </div>
@await Html.PartialAsync("_Sponsors", Model.Sponsors)
<div class="modal fade" data-bs-focus="false" id="extraFieldModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="extraFieldModal" 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="extraFieldModalContent"> <div class="modal-content" id="extraFieldModalContent">

View File

@@ -0,0 +1,73 @@
@using CarCareTracker.Helper
@model Sponsors
@inject ITranslationHelper translator
@inject IConfigHelper config
@{
var userConfig = config.GetUserConfig(User);
var enableAuth = userConfig.EnableAuth;
var userLanguage = userConfig.UserLanguage;
}
<div class="row">
<div class="d-flex justify-content-center">
<h6 class="display-6 mt-2">@translator.Translate(userLanguage, "Sponsors")</h6>
</div>
<hr />
<div class="col-12">
<div class="d-flex justify-content-center">
<p><a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://docs.lubelogger.com/Funding" target="_blank">Become a Sponsor</a></p>
</div>
</div>
@if (Model.LifeTime.Any())
{
<div class="col-12">
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Lifetime</h6>
</div>
<div class="d-flex justify-content-center">
<p class="lead">
@string.Join(", ", Model.LifeTime)
</p>
</div>
</div>
}
@if (Model.Gold.Any())
{
<div class="col-12">
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Gold</h6>
</div>
<div class="d-flex justify-content-center">
<p class="lead">
@string.Join(", ", Model.Gold)
</p>
</div>
</div>
}
@if (Model.Silver.Any())
{
<div class="col-12">
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Silver</h6>
</div>
<div class="d-flex justify-content-center">
<p class="lead">
@string.Join(", ", Model.Silver)
</p>
</div>
</div>
}
@if (Model.Bronze.Any())
{
<div class="col-12">
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Bronze</h6>
</div>
<div class="d-flex justify-content-center">
<p class="lead">
@string.Join(", ", Model.Bronze)
</p>
</div>
</div>
}
</div>

View File

@@ -70,8 +70,8 @@
} }
function globalParseFloat(input){ function globalParseFloat(input){
//remove thousands separator. //remove thousands separator.
var thousandSeparator = "@numberFormat.NumberGroupSeparator"; var thousandSeparator = decodeHTMLEntities("@numberFormat.NumberGroupSeparator");
var decimalSeparator = "@numberFormat.NumberDecimalSeparator"; var decimalSeparator = decodeHTMLEntities("@numberFormat.NumberDecimalSeparator");
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol"); var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
if (input == "---") { if (input == "---") {
input = "0"; input = "0";
@@ -85,13 +85,32 @@
return parseFloat(input); return parseFloat(input);
} }
function globalFloatToString(input) { function globalFloatToString(input) {
var decimalSeparator = "@numberFormat.NumberDecimalSeparator"; var decimalSeparator = decodeHTMLEntities("@numberFormat.NumberDecimalSeparator");
input = input.replace(".", decimalSeparator); input = input.replace(".", decimalSeparator);
return input; return input;
} }
function genericErrorMessage(){ function genericErrorMessage(){
return decodeHTMLEntities('@translator.Translate(userLanguage, "An error has occurred, please try again later")'); return decodeHTMLEntities('@translator.Translate(userLanguage, "An error has occurred, please try again later")');
} }
function globalAppendCurrency(input){
//check currency symbol position
var currencySymbolPosition = "@numberFormat.CurrencyPositivePattern";
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
switch (currencySymbolPosition) {
case "0":
return `${currencySymbol}${input}`;
break;
case "1":
return `${input}${currencySymbol}`;
break;
case "2":
return `${currencySymbol} ${input}`;
break;
case "3":
return `${input} ${currencySymbol}`;
break;
}
}
</script> </script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</head> </head>

View File

@@ -28,7 +28,7 @@
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('collisionRecordMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('collisionRecordMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
@@ -89,10 +89,9 @@
} }
<label for="collisionRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label> <label for="collisionRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="collisionRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="collisionRecordFiles">
<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>
} }
<div id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -151,7 +151,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'collisionRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="collisionRecordModalContent"> <div class="modal-content" id="collisionRecordModalContent">
</div> </div>

View File

@@ -0,0 +1,23 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<UploadedFiles>
<label id="documentsPendingUploadLabel">@translator.Translate(userLanguage, "Documents Pending Upload")</label>
<ul class="list-group" id="documentsPendingUploadList">
@foreach (UploadedFiles filesUploaded in Model)
{
<li class="list-group-item">
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
</div>
</li>
}
</ul>

View File

@@ -221,7 +221,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'gasRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="gasRecordModalContent"> <div class="modal-content" id="gasRecordModalContent">
</div> </div>

View File

@@ -17,23 +17,23 @@
consumptionUnit = "kWh"; consumptionUnit = "kWh";
} else if (useUKMPG) } else if (useUKMPG)
{ {
consumptionUnit = "liters"; consumptionUnit = @translator.Translate(userLanguage, "liters");
} }
else else
{ {
consumptionUnit = useMPG ? "gallons" : "liters"; consumptionUnit = useMPG ? @translator.Translate(userLanguage, "gallons") : @translator.Translate(userLanguage, "liters");
} }
if (useHours) if (useHours)
{ {
distanceUnit = "hours"; distanceUnit = @translator.Translate(userLanguage, "hours");
} }
else if (useUKMPG) else if (useUKMPG)
{ {
distanceUnit = "miles"; distanceUnit = @translator.Translate(userLanguage, "miles");
} }
else else
{ {
distanceUnit = useMPG ? "miles" : "kilometers"; distanceUnit = useMPG ? @translator.Translate(userLanguage, "miles") : @translator.Translate(userLanguage, "kilometers");
} }
} }
<div class="modal-header"> <div class="modal-header">
@@ -57,7 +57,7 @@
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('gasRecordMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('gasRecordMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
@@ -121,6 +121,7 @@
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="gasRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="gasRecordFiles">
<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 id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -45,6 +45,7 @@
<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>
} }
<div id="filesPendingUpload"></div>
</div> </div>
<div class="col-12"> <div class="col-12">
<label for="noteRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label> <label for="noteRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>

View File

@@ -61,7 +61,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="noteModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="noteModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'noteFiles')">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content" id="noteModalContent"> <div class="modal-content" id="noteModalContent">
</div> </div>

View File

@@ -23,14 +23,22 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="initialOdometerRecordMileage">@translator.Translate(userLanguage, "Initial Odometer")</label> <label for="initialOdometerRecordMileage">@translator.Translate(userLanguage, "Initial Odometer")</label>
<input type="number" inputmode="numeric" id="initialOdometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Initial Odometer reading")" value="@(Model.InitialMileage)"> <div class="input-group">
<input type="number" inputmode="numeric" id="initialOdometerRecordMileage" @(Model.InitialMileage != default ? "disabled" : "") class="form-control" placeholder="@translator.Translate(userLanguage,"Initial Odometer reading")" value="@(Model.InitialMileage)">
@if (Model.InitialMileage != default)
{
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-secondary zero-y-padding" onclick="toggleInitialOdometerEnabled()"><i class="bi bi-pencil"></i></button>
</div>
}
</div>
<label for="odometerRecordMileage">@translator.Translate(userLanguage,"Odometer")</label> <label for="odometerRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<div class="input-group"> <div class="input-group">
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading")" value="@(isNew ? "" : Model.Mileage)"> <input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading")" value="@(isNew ? "" : Model.Mileage)">
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('odometerRecordMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('odometerRecordMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
@@ -68,6 +76,7 @@
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="odometerRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="odometerRecordFiles">
<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 id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -151,7 +151,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="odometerRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="odometerRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'odometerRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="odometerRecordModalContent"> <div class="modal-content" id="odometerRecordModalContent">
</div> </div>

View File

@@ -73,10 +73,9 @@
{ {
<label for="planRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label> <label for="planRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles">
<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>
} }
<div id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -65,10 +65,9 @@
{ {
<label for="planRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label> <label for="planRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles">
<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>
} }
<div id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -97,14 +97,14 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="planRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="planRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'planRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="planRecordModalContent"> <div class="modal-content" id="planRecordModalContent">
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" data-bs-focus="false" id="planRecordTemplateModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="planRecordTemplateModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'planRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="planRecordTemplateModalContent"> <div class="modal-content" id="planRecordTemplateModalContent">
</div> </div>

View File

@@ -21,7 +21,7 @@
<input type="text" id="reminderDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Reminder Description")" value="@Model.Description"> <input type="text" id="reminderDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Reminder Description")" value="@Model.Description">
<label>@translator.Translate(userLanguage,"Remind me on")</label> <label>@translator.Translate(userLanguage,"Remind me on")</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricDate" value="@(ReminderMetric.Date)" checked="@(Model.Metric == ReminderMetric.Date)"> <input class="form-check-input" onclick="enableRecurring()" type="radio" name="reminderMetricOptions" id="reminderMetricDate" value="@(ReminderMetric.Date)" checked="@(Model.Metric == ReminderMetric.Date)">
<label class="form-check-label" for="reminderMetricDate">@translator.Translate(userLanguage,"Date")</label> <label class="form-check-label" for="reminderMetricDate">@translator.Translate(userLanguage,"Date")</label>
</div> </div>
<div class="input-group"> <div class="input-group">
@@ -29,7 +29,7 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricOdometer" value="@(ReminderMetric.Odometer)" checked="@(Model.Metric == ReminderMetric.Odometer)"> <input class="form-check-input" onclick="enableRecurring()" type="radio" name="reminderMetricOptions" id="reminderMetricOdometer" value="@(ReminderMetric.Odometer)" checked="@(Model.Metric == ReminderMetric.Odometer)">
<label class="form-check-label" for="reminderMetricOdometer">@translator.Translate(userLanguage,"Odometer")</label> <label class="form-check-label" for="reminderMetricOdometer">@translator.Translate(userLanguage,"Odometer")</label>
</div> </div>
<div class="input-group"> <div class="input-group">
@@ -37,12 +37,12 @@
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('reminderMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('reminderMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricBoth" value="@(ReminderMetric.Both)" checked="@(Model.Metric == ReminderMetric.Both)"> <input class="form-check-input" onclick="enableRecurring()" type="radio" name="reminderMetricOptions" id="reminderMetricBoth" value="@(ReminderMetric.Both)" checked="@(Model.Metric == ReminderMetric.Both)">
<label class="form-check-label" for="reminderMetricBoth">@translator.Translate(userLanguage,"Whichever comes first")</label> <label class="form-check-label" for="reminderMetricBoth">@translator.Translate(userLanguage,"Whichever comes first")</label>
</div> </div>
<div class="d-grid"></div> <div class="d-grid"></div>
@@ -62,7 +62,7 @@
<label class="form-check-label" for="reminderIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label> <label class="form-check-label" for="reminderIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label>
</div> </div>
<label for="reminderRecurringMileage">@translator.Translate(userLanguage,"Odometer")</label> <label for="reminderRecurringMileage">@translator.Translate(userLanguage,"Odometer")</label>
<select class="form-select" onchange="checkCustomMileageInterval()" id="reminderRecurringMileage" @(Model.IsRecurring ? "" : "disabled")> <select class="form-select" onchange="checkCustomMileageInterval()" id="reminderRecurringMileage" @(Model.IsRecurring && (Model.Metric == ReminderMetric.Odometer || Model.Metric == ReminderMetric.Both) ? "" : "disabled")>
<!option value="Other" @(Model.ReminderMileageInterval == ReminderMileageInterval.Other ? "selected" : "")>@(Model.ReminderMileageInterval == ReminderMileageInterval.Other && Model.CustomMileageInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMileageInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option> <!option value="Other" @(Model.ReminderMileageInterval == ReminderMileageInterval.Other ? "selected" : "")>@(Model.ReminderMileageInterval == ReminderMileageInterval.Other && Model.CustomMileageInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMileageInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option>
<!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option> <!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option>
<!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option> <!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option>
@@ -82,8 +82,8 @@
<!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option> <!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option>
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option> <!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
</select> </select>
<label for="reminderRecurringMonth">Month</label> <label for="reminderRecurringMonth">@translator.Translate(userLanguage, "Month")</label>
<select class="form-select" onchange="checkCustomMonthInterval()" id="reminderRecurringMonth" @(Model.IsRecurring ? "" : "disabled")> <select class="form-select" onchange="checkCustomMonthInterval()" id="reminderRecurringMonth" @(Model.IsRecurring && (Model.Metric == ReminderMetric.Date || Model.Metric == ReminderMetric.Both) ? "" : "disabled")>
<!option value="Other" @(Model.ReminderMonthInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.ReminderMonthInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option> <!option value="Other" @(Model.ReminderMonthInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.ReminderMonthInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option>
<!option value="OneMonth" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage, "1 Month")</!option> <!option value="OneMonth" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage, "1 Month")</!option>
<!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage,"3 Months")</!option> <!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage,"3 Months")</!option>

View File

@@ -73,18 +73,22 @@
{ {
<td class="col-1"><span class="text-success d-inline-block d-md-none"><i class="bi bi-hourglass-top h3"></i></span><span class="badge text-bg-success d-none d-md-inline-block">@translator.Translate(userLanguage, "Not Urgent")</span></td> <td class="col-1"><span class="text-success d-inline-block d-md-none"><i class="bi bi-hourglass-top h3"></i></span><span class="badge text-bg-success d-none d-md-inline-block">@translator.Translate(userLanguage, "Not Urgent")</span></td>
} }
@if (reminderRecord.Metric == ReminderMetric.Date) <td class="col-2">
{ <span data-column="metric" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true" data-bs-title="@($"<b><i class='bi bi-calendar-event me-2'></i></b>{reminderRecord.Date.ToShortDateString()}<b class='ms-2'><i class='bi bi-speedometer me-2'></i></b>{reminderRecord.Mileage}")">
<td class="col-2">@reminderRecord.Date.ToShortDateString()</td> @if (reminderRecord.Metric == ReminderMetric.Date)
} {
else if (reminderRecord.Metric == ReminderMetric.Odometer) @reminderRecord.Date.ToShortDateString()
{ }
<td class="col-2">@reminderRecord.Mileage</td> else if (reminderRecord.Metric == ReminderMetric.Odometer)
} {
else @reminderRecord.Mileage
{ }
<td class="col-2">@reminderRecord.Metric</td> else
} {
@reminderRecord.Metric
}
</span>
</td>
<td class="@(hasRefresh ? "col-3 col-md-4" : "col-5")" data-record-type='cost'>@reminderRecord.Description</td> <td class="@(hasRefresh ? "col-3 col-md-4" : "col-5")" data-record-type='cost'>@reminderRecord.Description</td>
<td class="col-2 col-md-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(reminderRecord.Notes)</td> <td class="col-2 col-md-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(reminderRecord.Notes)</td>
@if (hasRefresh) @if (hasRefresh)
@@ -118,4 +122,8 @@
<li><hr class="context-menu-multiple dropdown-divider"></li> <li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Delete")</a></li> <li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul> </ul>
<script>
$("[data-column='metric']").map((x, y) => new bootstrap.Tooltip(y));
</script>

View File

@@ -12,7 +12,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<select class="form-select" id="yearOption" onchange="yearUpdated()"> <select class="form-select" id="yearOption" onchange="yearUpdated()">
<option value="0">All Time</option> <option value="0">@translator.Translate(userLanguage, "All Time")</option>
@foreach (int year in Model.Years) @foreach (int year in Model.Years)
{ {
<option value="@year">@year</option> <option value="@year">@year</option>
@@ -32,7 +32,7 @@
<div class="col-12 col-md-10"> <div class="col-12 col-md-10">
<div class="dropdown d-grid dropdown-center"> <div class="dropdown d-grid dropdown-center">
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false"> <button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Metrics @translator.Translate(userLanguage, "Metrics")
</button> </button>
<ul class="dropdown-menu" style="width:100%;"> <ul class="dropdown-menu" style="width:100%;">
<li class="dropdown-item"> <li class="dropdown-item">
@@ -146,4 +146,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="vehicleHistoryReport" class="showOnPrint"></div> <div id="vehicleHistoryReport" class="showOnPrint"></div>
<script>
getSelectedMetrics();
</script>

View File

@@ -28,7 +28,7 @@
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('serviceRecordMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('serviceRecordMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
@@ -91,6 +91,7 @@
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="serviceRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="serviceRecordFiles">
<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 id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -149,7 +149,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'serviceRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="serviceRecordModalContent"> <div class="modal-content" id="serviceRecordModalContent">
</div> </div>

View File

@@ -34,7 +34,7 @@
<div class="input-group"> <div class="input-group">
<input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)"> <input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm zero-y-padding btn-primary" onclick="replenishSupplies()">+</button> <button type="button" class="btn btn-sm zero-y-padding btn-primary" onclick="replenishSupplies()"><i class="bi bi-plus"></i></button>
</div> </div>
</div> </div>
</div> </div>
@@ -77,6 +77,7 @@
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="supplyRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="supplyRecordFiles">
<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 id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -167,7 +167,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="supplyRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="supplyRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'supplyRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="supplyRecordModalContent"> <div class="modal-content" id="supplyRecordModalContent">
</div> </div>

View File

@@ -110,7 +110,7 @@
var inStockQuantity = globalParseFloat(inStock.text()); var inStockQuantity = globalParseFloat(inStock.text());
var unitPrice = globalParseFloat(priceField.text()); var unitPrice = globalParseFloat(priceField.text());
//validation //validation
if (isNaN(requestedQuantity) || requestedQuantity > inStockQuantity) { if (isNaN(requestedQuantity) || requestedQuantity > inStockQuantity || requestedQuantity <= 0) {
textField.addClass("is-invalid"); textField.addClass("is-invalid");
hasError = true; hasError = true;
} else { } else {
@@ -131,7 +131,7 @@
var parsedFloat = globalFloatToString(totalSum); var parsedFloat = globalFloatToString(totalSum);
$("#supplySumLabel").text(`Total: ${parsedFloat}`); $("#supplySumLabel").text(`Total: ${parsedFloat}`);
} }
$("#selectSuppliesButton").attr('disabled', (hasError || totalSum == 0)); $("#selectSuppliesButton").attr('disabled', (hasError || selectedSupplies.toArray().length == 0));
if (!hasError) { if (!hasError) {
return { return {
totalSum: globalFloatToString(totalSum), totalSum: globalFloatToString(totalSum),

View File

@@ -92,6 +92,7 @@
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="taxRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="taxRecordFiles">
<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 id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -143,7 +143,7 @@
</div> </div>
<div class="modal fade" data-bs-focus="false" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'taxRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="taxRecordModalContent"> <div class="modal-content" id="taxRecordModalContent">
</div> </div>

View File

@@ -28,7 +28,7 @@
@if (isNew) @if (isNew)
{ {
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('upgradeRecordMileage')">+</button> <button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('upgradeRecordMileage')"><i class="bi bi-plus"></i></button>
</div> </div>
} }
</div> </div>
@@ -89,10 +89,9 @@
} }
<label for="upgradeRecordFiles">Upload documents(optional)</label> <label for="upgradeRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="upgradeRecordFiles"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="upgradeRecordFiles">
<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>
} }
<div id="filesPendingUpload"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -109,7 +109,7 @@
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 flex-grow-1 col-xl-1" data-column="date">@translator.Translate(userLanguage, "Date")</th> <th scope="col" class="col-2 flex-grow-1 col-xl-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 flex-grow-1 col-xl-4" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 col-xl-4" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
@@ -124,7 +124,7 @@
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@upgradeRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditUpgradeRecordModal,@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'> <tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@upgradeRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditUpgradeRecordModal,@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
<td class="col-2 flex-grow-1 col-xl-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(upgradeRecord.Date)">@upgradeRecord.Date.ToShortDateString()</td> <td class="col-2 flex-grow-1 col-xl-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(upgradeRecord.Date)">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@upgradeRecord.Mileage</td> <td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@upgradeRecord.Mileage</td>
<td class="col-3 flex-grow-1 col-xl-4" data-column="description">@upgradeRecord.Description</td> <td class="col-3 flex-grow-1 flex-shrink-1 col-xl-4" data-column="description">@upgradeRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td> <td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
@@ -150,7 +150,7 @@
</div> </div>
</div> </div>
<div class="modal fade" data-bs-focus="false" id="upgradeRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="upgradeRecordModal" tabindex="-1" role="dialog" aria-hidden="true" onpaste="handleModalPaste(event, 'upgradeRecordFiles')">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="upgradeRecordModalContent"> <div class="modal-content" id="upgradeRecordModalContent">
</div> </div>

View File

@@ -6,8 +6,8 @@
var userLanguage = userConfig.UserLanguage; var userLanguage = userConfig.UserLanguage;
} }
@model List<UploadedFiles> @model List<UploadedFiles>
<label>@translator.Translate(userLanguage, "Uploaded Documents")</label> <label id="uploadedDocumentsLabel">@translator.Translate(userLanguage, "Uploaded Documents")</label>
<ul class="list-group"> <ul class="list-group" id="uploadedDocumentsList">
@foreach (UploadedFiles filesUploaded in Model) @foreach (UploadedFiles filesUploaded in Model)
{ {
<li class="list-group-item"> <li class="list-group-item">

View File

@@ -31,6 +31,10 @@
{ {
<span><i class="bi bi-ev-station me-2"></i>@translator.Translate(userLanguage, "Electric")</span> <span><i class="bi bi-ev-station me-2"></i>@translator.Translate(userLanguage, "Electric")</span>
} }
else if (Model.VehicleData.IsDiesel)
{
<span><i class="bi bi-fuel-pump-diesel me-2"></i>@translator.Translate(userLanguage, "Diesel")</span>
}
else else
{ {
<span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span> <span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span>

View File

@@ -46,10 +46,12 @@
} }
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="form-check form-switch"> <label for="inputFuelType">@translator.Translate(userLanguage, "Fuel Type")</label>
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric"> <select class="form-select" onchange="checkCustomMonthInterval()" id="inputFuelType")>
<label class="form-check-label" for="inputIsElectric">@translator.Translate(userLanguage, "Electric Vehicle")</label> <!option value="Gasoline" @(!Model.IsDiesel && !Model.IsElectric ? "selected" : "")>@translator.Translate(userLanguage, "Gasoline")</!option>
</div> <!option value="Diesel" @(Model.IsDiesel ? "selected" : "")>@translator.Translate(userLanguage, "Diesel")</!option>
<!option value="Electric" @(Model.IsElectric ? "selected" : "")>@translator.Translate(userLanguage, "Electric")</!option>
</select>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputUseHours" checked="@Model.UseHours"> <input class="form-check-input" type="checkbox" role="switch" id="inputUseHours" checked="@Model.UseHours">
<label class="form-check-label" for="inputUseHours">@translator.Translate(userLanguage, "Use Engine Hours")</label> <label class="form-check-label" for="inputUseHours">@translator.Translate(userLanguage, "Use Engine Hours")</label>

View File

@@ -30,6 +30,7 @@ services:
POSTGRES_PASSWORD: "lubepass" POSTGRES_PASSWORD: "lubepass"
POSTGRES_DB: "lubelogger" POSTGRES_DB: "lubelogger"
volumes: volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro

6
init.sql Normal file
View File

@@ -0,0 +1,6 @@
DO $$
BEGIN
IF NOT EXISTS (SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app') THEN
CREATE SCHEMA app;
END IF;
END $$;

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -341,6 +341,47 @@ function showAccountInformationModal() {
$('#accountInformationModal').modal('show'); $('#accountInformationModal').modal('show');
}) })
} }
function showRootAccountInformationModal() {
$.get('/Home/GetRootAccountInformationModal', function (data) {
$('#accountInformationModalContent').html(data);
$('#accountInformationModal').modal('show');
})
}
function validateAndSaveRootUserAccount() {
var hasError = false;
if ($('#inputUsername').val().trim() == '') {
$('#inputUsername').addClass("is-invalid");
hasError = true;
} else {
$('#inputUsername').removeClass("is-invalid");
}
if ($('#inputPassword').val().trim() == '') {
$('#inputPassword').addClass("is-invalid");
hasError = true;
} else {
$('#inputPassword').removeClass("is-invalid");
}
if (hasError) {
errorToast("Please check the form data");
return;
}
var userAccountInfo = {
userName: $('#inputUsername').val(),
password: $('#inputPassword').val()
}
$.post('/Login/CreateLoginCreds', { credentials: userAccountInfo }, function (data) {
if (data) {
//hide modal
hideAccountInformationModal();
successToast('Root Account Updated');
performLogOut();
} else {
errorToast(data.message);
}
});
}
function hideAccountInformationModal() { function hideAccountInformationModal() {
$('#accountInformationModal').modal('hide'); $('#accountInformationModal').modal('hide');
} }

View File

@@ -175,28 +175,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "l": case "l":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 3.785; var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "l")); sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
sender.attr("data-unit", "l"); sender.attr("data-unit", "l");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 3.785; var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
case "imp gal": case "imp gal":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 1.201; var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal")); sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
sender.attr("data-unit", "imp gal"); sender.attr("data-unit", "imp gal");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 1.201; var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
@@ -206,28 +206,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "US gal": case "US gal":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 3.785; var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal")); sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
sender.attr("data-unit", "US gal"); sender.attr("data-unit", "US gal");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 3.785; var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
case "imp gal": case "imp gal":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 4.546; var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal")); sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
sender.attr("data-unit", "imp gal"); sender.attr("data-unit", "imp gal");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 4.546; var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
@@ -237,28 +237,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "US gal": case "US gal":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 1.201; var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal")); sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
sender.attr("data-unit", "US gal"); sender.attr("data-unit", "US gal");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 1.201; var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
case "l": case "l":
$("[data-gas-type='consumption']").map((index, elem) => { $("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 4.546; var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "l")); sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
sender.attr("data-unit", "l"); sender.attr("data-unit", "l");
}); });
$("[data-gas-type='unitcost']").map((index, elem) => { $("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 4.546; var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`; elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
}); });
if (save) { setDebounce(saveUserGasTabPreferences); } if (save) { setDebounce(saveUserGasTabPreferences); }
break; break;
@@ -275,7 +275,7 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
var convertedAmount = globalParseFloat(elem.innerText); var convertedAmount = globalParseFloat(elem.innerText);
if (convertedAmount > 0) { if (convertedAmount > 0) {
convertedAmount = 100 / convertedAmount; convertedAmount = 100 / convertedAmount;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
} }
}); });
//update labels up top. //update labels up top.
@@ -283,19 +283,19 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
if (newAverage > 0) { if (newAverage > 0) {
newAverage = 100 / newAverage; newAverage = 100 / newAverage;
var averageLabel = $("#averageFuelMileageLabel"); var averageLabel = $("#averageFuelMileageLabel");
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${newAverage.toFixed(2)}`); averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(newAverage.toFixed(2))}`);
} }
var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim()); var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim());
if (newMin > 0) { if (newMin > 0) {
newMin = 100 / newMin; newMin = 100 / newMin;
var minLabel = $("#minFuelMileageLabel"); var minLabel = $("#minFuelMileageLabel");
minLabel.text(`${minLabel.text().split(':')[0]}: ${newMin.toFixed(2)}`); minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(newMin.toFixed(2))}`);
} }
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim()); var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim());
if (newMax > 0) { if (newMax > 0) {
newMax = 100 / newMax; newMax = 100 / newMax;
var maxLabel = $("#maxFuelMileageLabel"); var maxLabel = $("#maxFuelMileageLabel");
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${newMax.toFixed(2)}`); maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(newMax.toFixed(2))}`);
} }
sender.text(sender.text().replace(sender.attr("data-unit"), "km/l")); sender.text(sender.text().replace(sender.attr("data-unit"), "km/l"));
sender.attr("data-unit", "km/l"); sender.attr("data-unit", "km/l");
@@ -309,27 +309,26 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
var convertedAmount = globalParseFloat(elem.innerText); var convertedAmount = globalParseFloat(elem.innerText);
if (convertedAmount > 0) { if (convertedAmount > 0) {
convertedAmount = 100 / convertedAmount; convertedAmount = 100 / convertedAmount;
elem.innerText = convertedAmount.toFixed(2); elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
} }
}); });
var newAverage = globalParseFloat($("#averageFuelMileageLabel").text().split(":")[1].trim()); var newAverage = globalParseFloat($("#averageFuelMileageLabel").text().split(":")[1].trim());
if (newAverage > 0) { if (newAverage > 0) {
newAverage = 100 / newAverage; newAverage = 100 / newAverage;
var averageLabel = $("#averageFuelMileageLabel"); var averageLabel = $("#averageFuelMileageLabel");
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${newAverage.toFixed(2)}`); averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(newAverage.toFixed(2))}`);
} }
var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim()); var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim());
if (newMin > 0) { if (newMin > 0) {
newMin = 100 / newMin; newMin = 100 / newMin;
var minLabel = $("#minFuelMileageLabel"); var minLabel = $("#minFuelMileageLabel");
minLabel.text(`${minLabel.text().split(':')[0]}: ${newMin.toFixed(2)}`); minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(newMin.toFixed(2))}`);
} }
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim()); var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim());
if (newMax > 0) { if (newMax > 0) {
newMax = 100 / newMax; newMax = 100 / newMax;
var maxLabel = $("#maxFuelMileageLabel"); var maxLabel = $("#maxFuelMileageLabel");
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${newMax.toFixed(2)}`); maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(newMax.toFixed(2))}`);
} }
sender.text(sender.text().replace(sender.attr("data-unit"), "l/100km")); sender.text(sender.text().replace(sender.attr("data-unit"), "l/100km"));
sender.attr("data-unit", "l/100km"); sender.attr("data-unit", "l/100km");
@@ -358,17 +357,17 @@ function updateMPGLabels() {
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;
} }
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${averageMPG.toFixed(2)}`); averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(averageMPG.toFixed(2))}`);
} else { } else {
averageLabel.text(`${averageLabel.text().split(':')[0]}: 0.00`); averageLabel.text(`${averageLabel.text().split(':')[0]}: 0.00`);
} }
if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l') { if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l') {
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${minMPG.toFixed(2)}`); maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${maxMPG.toFixed(2)}`); minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`);
} }
else { else {
minLabel.text(`${minLabel.text().split(':')[0]}: ${minMPG.toFixed(2)}`); minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${maxMPG.toFixed(2)}`); maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`);
} }
} }
} }

View File

@@ -208,4 +208,12 @@ function saveMultipleOdometerRecordsToVehicle() {
errorToast(genericErrorMessage()); errorToast(genericErrorMessage());
} }
}) })
}
function toggleInitialOdometerEnabled() {
if ($("#initialOdometerRecordMileage").prop("disabled")) {
$("#initialOdometerRecordMileage").prop("disabled", false);
} else {
$("#initialOdometerRecordMileage").prop("disabled", true);
}
} }

View File

@@ -137,8 +137,20 @@ function appendMileageToOdometer(increment) {
function enableRecurring() { function enableRecurring() {
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked"); var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
if (reminderIsRecurring) { if (reminderIsRecurring) {
$("#reminderRecurringMileage").attr('disabled', false); //check selected metric
$("#reminderRecurringMonth").attr('disabled', false); var reminderMetric = $('#reminderOptions input:radio:checked').val();
if (reminderMetric == "Date") {
$("#reminderRecurringMonth").attr('disabled', false);
$("#reminderRecurringMileage").attr('disabled', true);
}
else if (reminderMetric == "Odometer") {
$("#reminderRecurringMileage").attr('disabled', false);
$("#reminderRecurringMonth").attr('disabled', true);
}
else if (reminderMetric == "Both") {
$("#reminderRecurringMonth").attr('disabled', false);
$("#reminderRecurringMileage").attr('disabled', false);
}
} else { } else {
$("#reminderRecurringMileage").attr('disabled', true); $("#reminderRecurringMileage").attr('disabled', true);
$("#reminderRecurringMonth").attr('disabled', true); $("#reminderRecurringMonth").attr('disabled', true);

View File

@@ -1,5 +1,5 @@
function getYear() { function getYear() {
return $("#yearOption").val(); return $("#yearOption").val() ?? '0';
} }
function generateVehicleHistoryReport() { function generateVehicleHistoryReport() {
var vehicleId = GetVehicleId().vehicleId; var vehicleId = GetVehicleId().vehicleId;
@@ -29,6 +29,43 @@ function refreshMPGChart() {
$("#monthFuelMileageReportContent").html(data); $("#monthFuelMileageReportContent").html(data);
}) })
} }
function setSelectedMetrics() {
var selectedMetricCheckBoxes = [];
$(".reportCheckBox:checked").map((index, elem) => {
selectedMetricCheckBoxes.push(elem.id);
});
var yearMetric = $('#yearOption').val();
var reminderMetric = $("#reminderOption").val();
sessionStorage.setItem("selectedMetricCheckBoxes", JSON.stringify(selectedMetricCheckBoxes));
sessionStorage.setItem("yearMetric", yearMetric);
sessionStorage.setItem("reminderMetric", reminderMetric);
}
function getSelectedMetrics() {
var selectedMetricCheckBoxes = sessionStorage.getItem("selectedMetricCheckBoxes");
var yearMetric = sessionStorage.getItem("yearMetric");
var reminderMetric = sessionStorage.getItem("reminderMetric");
if (selectedMetricCheckBoxes != null && yearMetric != null && reminderMetric != null) {
selectedMetricCheckBoxes = JSON.parse(selectedMetricCheckBoxes);
$(".reportCheckBox").prop('checked', false);
$("#selectAllExpenseCheck").prop("checked", false);
selectedMetricCheckBoxes.map(x => {
$(`#${x}`).prop('checked', true);
});
if (selectedMetricCheckBoxes.length == 6) {
$("#selectAllExpenseCheck").prop("checked", true);
}
//check if option is available
if ($("#yearOption").has(`option[value=${yearMetric}]`).length > 0) {
$('#yearOption').val(yearMetric);
}
$("#reminderOption").val(reminderMetric);
//retrieve data.
yearUpdated();
updateReminderPie();
return true;
}
return false;
}
function refreshBarChart() { function refreshBarChart() {
var selectedMetrics = []; var selectedMetrics = [];
var vehicleId = GetVehicleId().vehicleId; var vehicleId = GetVehicleId().vehicleId;
@@ -61,11 +98,13 @@ function refreshBarChart() {
}, function (data) { }, function (data) {
$("#gasCostByMonthReportContent").html(data); $("#gasCostByMonthReportContent").html(data);
refreshMPGChart(); refreshMPGChart();
}); });
setSelectedMetrics();
} }
function updateReminderPie() { function updateReminderPie() {
var vehicleId = GetVehicleId().vehicleId; var vehicleId = GetVehicleId().vehicleId;
var daysToAdd = $("#reminderOption").val(); var daysToAdd = $("#reminderOption").val();
setSelectedMetrics();
$.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) { $.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) {
$("#reminderMakeUpReportContent").html(data); $("#reminderMakeUpReportContent").html(data);
}); });

View File

@@ -40,7 +40,8 @@ function saveVehicle(isEdit) {
var vehiclePurchaseDate = $("#inputPurchaseDate").val(); var vehiclePurchaseDate = $("#inputPurchaseDate").val();
var vehicleSoldDate = $("#inputSoldDate").val(); var vehicleSoldDate = $("#inputSoldDate").val();
var vehicleLicensePlate = $("#inputLicensePlate").val(); var vehicleLicensePlate = $("#inputLicensePlate").val();
var vehicleIsElectric = $("#inputIsElectric").is(":checked"); var vehicleIsElectric = $("#inputFuelType").val() == 'Electric';
var vehicleIsDiesel = $("#inputFuelType").val() == 'Diesel';
var vehicleUseHours = $("#inputUseHours").is(":checked"); var vehicleUseHours = $("#inputUseHours").is(":checked");
var vehicleHasOdometerAdjustment = $("#inputHasOdometerAdjustment").is(':checked'); var vehicleHasOdometerAdjustment = $("#inputHasOdometerAdjustment").is(':checked');
var vehicleOdometerMultiplier = $("#inputOdometerMultiplier").val(); var vehicleOdometerMultiplier = $("#inputOdometerMultiplier").val();
@@ -119,6 +120,7 @@ function saveVehicle(isEdit) {
model: vehicleModel, model: vehicleModel,
licensePlate: vehicleLicensePlate, licensePlate: vehicleLicensePlate,
isElectric: vehicleIsElectric, isElectric: vehicleIsElectric,
isDiesel: vehicleIsDiesel,
tags: vehicleTags, tags: vehicleTags,
useHours: vehicleUseHours, useHours: vehicleUseHours,
extraFields: extraFields.extraFields, extraFields: extraFields.extraFields,
@@ -178,8 +180,8 @@ function uploadFileAsync(event) {
}); });
} }
function isValidMoney(input) { function isValidMoney(input) {
const euRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}(\.?\d{3})?(,\d{1,3}?)?\)?$/; const euRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((\.\d{3}){0,8}|(\d{3}){0,8})(,\d{1,3}?)?\)?$/;
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}(,?\d{3})?(\.\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 initDatePicker(input, futureOnly) { function initDatePicker(input, futureOnly) {
@@ -339,7 +341,7 @@ function updateAggregateLabels() {
if (labelsToSum.length > 0) { if (labelsToSum.length > 0) {
newSum = labelsToSum.map(x => globalParseFloat(x.textContent)).reduce((a, b,) => a + b).toFixed(2); newSum = labelsToSum.map(x => globalParseFloat(x.textContent)).reduce((a, b,) => a + b).toFixed(2);
} }
sumLabel.text(`${sumLabel.text().split(':')[0]}: ${getGlobalConfig().currencySymbol}${newSum}`) sumLabel.text(`${sumLabel.text().split(':')[0]}: ${globalAppendCurrency(globalFloatToString(newSum))}`)
} }
//Sum Distance //Sum Distance
var sumDistanceLabel = $("[data-aggregate-type='sum-distance']"); var sumDistanceLabel = $("[data-aggregate-type='sum-distance']");
@@ -373,8 +375,12 @@ function uploadVehicleFilesAsync(event) {
type: 'POST', type: 'POST',
success: function (response) { success: function (response) {
sloader.hide(); sloader.hide();
$(event).val(""); //clear out the filename from the uploader
if (response.length > 0) { if (response.length > 0) {
uploadedFiles.push.apply(uploadedFiles, response); uploadedFiles.push.apply(uploadedFiles, response);
$.post('/Vehicle/GetFilesPendingUpload', { uploadedFiles: uploadedFiles }, function (viewData) {
$("#filesPendingUpload").html(viewData);
});
} }
}, },
error: function () { error: function () {
@@ -386,6 +392,15 @@ function uploadVehicleFilesAsync(event) {
function deleteFileFromUploadedFiles(fileLocation, event) { function deleteFileFromUploadedFiles(fileLocation, event) {
event.parentElement.parentElement.parentElement.remove(); event.parentElement.parentElement.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation); uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
if (fileLocation.startsWith("/temp/")) {
if ($("#documentsPendingUploadList > li").length == 0) {
$("#documentsPendingUploadLabel").text("");
}
} else if (fileLocation.startsWith("/documents/")) {
if ($("#uploadedDocumentsList > li").length == 0) {
$("#uploadedDocumentsLabel").text("");
}
}
} }
function editFileName(fileLocation, event) { function editFileName(fileLocation, event) {
Swal.fire({ Swal.fire({
@@ -1053,4 +1068,24 @@ function bindModalInputChanges(modalName) {
$(`#${modalName} select, #${modalName} input[type='checkbox']`).off('input').on('input', function (e) { $(`#${modalName} select, #${modalName} input[type='checkbox']`).off('input').on('input', function (e) {
$(e.currentTarget).attr('data-changed', true); $(e.currentTarget).attr('data-changed', true);
}); });
}
function handleModalPaste(e, recordType) {
var clipboardFiles = e.clipboardData.files;
var acceptableFileFormats = $(`#${recordType}`).attr("accept");
var acceptableFileFormatsArray = acceptableFileFormats.split(',');
var acceptableFiles = new DataTransfer();
if (clipboardFiles.length > 0) {
for (var x = 0; x < clipboardFiles.length; x++) {
if (acceptableFileFormats != "*") {
var fileExtension = `.${clipboardFiles[x].name.split('.').pop()}`;
if (acceptableFileFormatsArray.includes(fileExtension)) {
acceptableFiles.items.add(clipboardFiles[x]);
}
} else {
acceptableFiles.items.add(clipboardFiles[x]);
}
}
$(`#${recordType}`)[0].files = acceptableFiles.files;
$(`#${recordType}`).trigger('change');
}
} }

View File

@@ -480,19 +480,19 @@ function getRecordsDeltaStats(recordIds) {
var diffOdo = maxOdo - minOdo; var diffOdo = maxOdo - minOdo;
var diffDate = maxDate - minDate; var diffDate = maxDate - minDate;
var divisibleCount = recordIds.length - 1; var divisibleCount = recordIds.length - 1;
var averageOdo = diffOdo > 0 ? (diffOdo / divisibleCount).toFixed(2) : 0; var averageOdo = diffOdo > 0 ? (diffOdo / divisibleCount).toFixed(2) : '0';
var averageDays = diffDate > 0 ? Math.floor((diffDate / divisibleCount) / 8.64e7) : 0; var averageDays = diffDate > 0 ? Math.floor((diffDate / divisibleCount) / 8.64e7) : '0';
var averageSum = costSum > 0 ? (costSum / recordIds.length).toFixed(2) : 0; var averageSum = costSum > 0 ? (costSum / recordIds.length).toFixed(2) : '0';
costSum = costSum.toFixed(2); costSum = costSum.toFixed(2);
Swal.fire({ Swal.fire({
title: "Record Statistics", title: "Record Statistics",
html: `<p>Average Distance Traveled between Records: ${averageOdo}</p> html: `<p>Average Distance Traveled between Records: ${globalFloatToString(averageOdo)}</p>
<br /> <br />
<p>Average Days between Records: ${averageDays}</p> <p>Average Days between Records: ${averageDays}</p>
<br /> <br />
<p>Total Cost: ${getGlobalConfig().currencySymbol} ${costSum}</p> <p>Total Cost: ${globalAppendCurrency(globalFloatToString(costSum))}</p>
<br /> <br />
<p>Average Cost: ${getGlobalConfig().currencySymbol} ${averageSum}</p>` <p>Average Cost: ${globalAppendCurrency(globalFloatToString(averageSum))}</p>`
, ,
icon: "info" icon: "info"
}); });

View File

@@ -341,14 +341,14 @@
break; break;
//COPY EVENT //COPY EVENT
case 67: case 67:
if (event.ctrlKey) { if (event.ctrlKey || event.metaKey) {
event.preventDefault(); event.preventDefault();
navigator.clipboard.writeText(self.itemsArray.join(" ")); navigator.clipboard.writeText(self.itemsArray.join(" "));
} }
break; break;
//PASTE EVENT //PASTE EVENT
case 86: case 86:
if (event.ctrlKey) { if (event.ctrlKey || event.metaKey) {
setTimeout(function () { setTimeout(function () {
var pastedString = $input.val(); var pastedString = $input.val();
//clear pasted string. //clear pasted string.

View File

@@ -6,22 +6,44 @@
{ {
"src": "/defaults/lubelogger_icon_72.png", "src": "/defaults/lubelogger_icon_72.png",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png",
"purpose": "any"
},
{
"src": "/defaults/lubelogger_maskable_icon_72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable"
}, },
{ {
"src": "/defaults/lubelogger_icon_128.png", "src": "/defaults/lubelogger_icon_128.png",
"sizes": "128x128", "sizes": "128x128",
"type": "image/png" "type": "image/png",
"purpose": "any"
},
{
"src": "/defaults/lubelogger_maskable_icon_128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable"
}, },
{ {
"src": "/defaults/lubelogger_icon_144.png", "src": "/defaults/lubelogger_icon_144.png",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png",
"purpose": "any"
}, },
{ {
"src": "/defaults/lubelogger_icon_192.png", "src": "/defaults/lubelogger_icon_192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png",
"purpose": "any"
},
{
"src": "/defaults/lubelogger_maskable_icon_192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
} }
], ],
"screenshots": [ "screenshots": [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long