Compare commits

..

85 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
87 changed files with 1140 additions and 10688 deletions

1
.env
View File

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

View File

@@ -13,7 +13,8 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<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" />
</ItemGroup>

View File

@@ -21,6 +21,9 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
private readonly IPlanRecordDataAccess _planRecordDataAccess;
private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess;
private readonly IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
@@ -42,6 +45,9 @@ namespace CarCareTracker.Controllers
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
ISupplyRecordDataAccess supplyRecordDataAccess,
IPlanRecordDataAccess planRecordDataAccess,
IPlanRecordTemplateDataAccess planRecordTemplateDataAccess,
IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper,
@@ -60,6 +66,9 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_supplyRecordDataAccess = supplyRecordDataAccess;
_planRecordDataAccess = planRecordDataAccess;
_planRecordTemplateDataAccess = planRecordTemplateDataAccess;
_userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper;
@@ -95,14 +104,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/servicerecords")]
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 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);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/servicerecords/add")]
public IActionResult AddServiceRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddServiceRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -131,7 +148,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -163,14 +181,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/repairrecords")]
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 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);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/repairrecords/add")]
public IActionResult AddRepairRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddRepairRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -199,7 +225,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -231,14 +258,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/upgraderecords")]
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 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);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/upgraderecords/add")]
public IActionResult AddUpgradeRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddUpgradeRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -267,7 +302,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -299,7 +335,15 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/taxrecords")]
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);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -332,7 +376,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord);
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")]
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);
return Json(result);
}
@@ -361,13 +414,21 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/odometerrecords")]
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);
//determine if conversion is needed.
if (vehicleRecords.All(x => x.InitialMileage == default))
{
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);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -399,7 +460,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date),
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),
Mileage = int.Parse(input.Odometer)
Mileage = int.Parse(input.Odometer),
ExtraFields = input.ExtraFields
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
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")]
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 result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG)
.Select(x => new GasRecordExportModel {
@@ -429,7 +499,8 @@ namespace CarCareTracker.Controllers
FuelEconomy = x.MilesPerGallon.ToString(),
IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes
Notes = x.Notes,
ExtraFields = x.ExtraFields
});
return Json(result);
}
@@ -470,7 +541,8 @@ namespace CarCareTracker.Controllers
IsFillToFull = bool.Parse(input.IsFillToFull),
MissedFuelUp = bool.Parse(input.MissedFuelUp),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -502,9 +574,17 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/reminders")]
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 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);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
@@ -562,6 +642,49 @@ namespace CarCareTracker.Controllers
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[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")]
public IActionResult RestoreDemo()
{

View File

@@ -69,6 +69,7 @@ namespace CarCareTracker.Controllers
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
@@ -105,7 +106,7 @@ namespace CarCareTracker.Controllers
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, 0, DateTime.Now).FirstOrDefault();
return PartialView("_ReminderRecordCalendarModal", reminderUrgency);
}
public IActionResult Settings()
public async Task<IActionResult> Settings()
{
var userConfig = _config.GetUserConfig(User);
var languages = _fileHelper.GetLanguages();
@@ -114,6 +115,16 @@ namespace CarCareTracker.Controllers
UserConfig = userConfig,
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);
}
[HttpPost]
@@ -209,6 +220,13 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name;
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)]
public IActionResult Error()
{

View File

@@ -34,10 +34,16 @@ namespace CarCareTracker.Controllers
{
var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
remoteAuthConfig.State = generatedState;
var pkceKeyPair = _loginLogic.GetPKCEChallengeCode();
remoteAuthConfig.CodeChallenge = pkceKeyPair.Value;
if (remoteAuthConfig.ValidateState)
{
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;
return Redirect(remoteAuthURL);
}
@@ -60,10 +66,16 @@ namespace CarCareTracker.Controllers
var remoteAuthConfig = _config.GetOpenIDConfig();
var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
remoteAuthConfig.State = generatedState;
var pkceKeyPair = _loginLogic.GetPKCEChallengeCode();
remoteAuthConfig.CodeChallenge = pkceKeyPair.Value;
if (remoteAuthConfig.ValidateState)
{
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;
return Json(remoteAuthURL);
}
@@ -99,6 +111,16 @@ namespace CarCareTracker.Controllers
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
};
if (openIdConfig.UsePKCE)
{
//retrieve stored challenge verifier
var storedVerifier = Request.Cookies["OIDC_VERIFIER"];
if (!string.IsNullOrWhiteSpace(storedVerifier))
{
httpParams.Add(new KeyValuePair<string, string>("code_verifier", storedVerifier));
Response.Cookies.Delete("OIDC_VERIFIER");
}
}
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
{
Content = new FormUrlEncodedContent(httpParams)
@@ -137,6 +159,11 @@ namespace CarCareTracker.Controllers
} else
{
_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
{
@@ -220,7 +247,7 @@ namespace CarCareTracker.Controllers
var result = _loginLogic.ResetPasswordByUser(credentials);
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]
public IActionResult CreateLoginCreds(LoginModel credentials)
{
@@ -235,7 +262,7 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[Authorize]
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult DestroyLoginCreds()
{

View File

@@ -219,37 +219,53 @@ namespace CarCareTracker.Controllers
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
if (mode == ImportMode.ServiceRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
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 csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
//custom writer
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
writer.Dispose();
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.RepairRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
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 csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -257,17 +273,23 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.UpgradeRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
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 csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -275,17 +297,22 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.OdometerRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
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 csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -293,8 +320,6 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.SupplyRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
@@ -307,13 +332,14 @@ namespace CarCareTracker.Controllers
PartQuantity = x.Quantity.ToString(),
PartSupplier = x.PartSupplier,
Notes = x.Notes,
Tags = string.Join(" ", x.Tags)
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -321,17 +347,22 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.TaxRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
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 csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteTaxRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -339,8 +370,6 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.PlanRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
@@ -353,13 +382,14 @@ namespace CarCareTracker.Controllers
Type = x.ImportMode.ToString(),
Priority = x.Priority.ToString(),
Progress = x.Progress.ToString(),
Notes = x.Notes
Notes = x.Notes,
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WritePlanRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -367,8 +397,6 @@ namespace CarCareTracker.Controllers
}
else if (mode == ImportMode.GasRecord)
{
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
@@ -383,13 +411,14 @@ namespace CarCareTracker.Controllers
IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes,
Tags = string.Join(" ", x.Tags)
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(exportData);
StaticHelper.WriteGasRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
@@ -417,7 +446,7 @@ namespace CarCareTracker.Controllers
{
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.HeaderValidated = null;
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
@@ -427,6 +456,7 @@ namespace CarCareTracker.Controllers
var records = csv.GetRecords<ImportModel>().ToList();
if (records.Any())
{
var requiredExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)mode).ExtraFields.Where(x=>x.IsRequired).Select(y=>y.Name);
foreach (ImportModel importModel in records)
{
if (mode == ImportMode.GasRecord)
@@ -439,7 +469,8 @@ namespace CarCareTracker.Controllers
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
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))
{
@@ -495,7 +526,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
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);
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)),
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
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);
}
@@ -537,7 +570,8 @@ namespace CarCareTracker.Controllers
Priority = parsedPriority,
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description,
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);
}
@@ -551,7 +585,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
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);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -575,7 +610,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
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);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -601,7 +637,8 @@ namespace CarCareTracker.Controllers
Description = importModel.Description,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
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);
}
@@ -614,7 +651,8 @@ namespace CarCareTracker.Controllers
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
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);
}
@@ -1153,6 +1191,10 @@ namespace CarCareTracker.Controllers
{
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 yearDifference = DateTime.Now.Year - minYear + 1;
for (int i = 0; i < yearDifference; i++)
@@ -1783,6 +1825,7 @@ namespace CarCareTracker.Controllers
[HttpPost]
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);
if (result)
{
@@ -2379,6 +2422,12 @@ namespace CarCareTracker.Controllers
#endregion
#region "Shared Methods"
[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))]
public IActionResult SearchRecords(int vehicleId, string searchQuery)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ namespace CarCareTracker.Helper
bool RestoreBackup(string fileName, bool clearExisting = false);
string MakeAttachmentsExport(List<GenericReportModel> exportData);
List<string> GetLanguages();
int ClearTempFolder();
int ClearUnlinkedThumbnails(List<string> linkedImages);
int ClearUnlinkedDocuments(List<string> linkedDocuments);
}
public class FileHelper : IFileHelper
{
@@ -314,5 +317,56 @@ namespace CarCareTracker.Helper
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 System.Net.Mail;
using System.Net;
using MimeKit;
using MailKit.Net.Smtp;
namespace CarCareTracker.Helper
{
@@ -15,13 +15,16 @@ namespace CarCareTracker.Helper
{
private readonly MailConfig mailConfig;
private readonly IFileHelper _fileHelper;
private readonly ILogger<MailHelper> _logger;
public MailHelper(
IConfiguration config,
IFileHelper fileHelper
IFileHelper fileHelper,
ILogger<MailHelper> logger
) {
//load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
mailConfig = config.GetSection("MailConfig").Get<MailConfig>() ?? new MailConfig();
_fileHelper = fileHelper;
_logger = logger;
}
public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{
@@ -34,7 +37,7 @@ namespace CarCareTracker.Helper
}
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}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -55,7 +58,7 @@ namespace CarCareTracker.Helper
}
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}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
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 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)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -116,43 +119,48 @@ namespace CarCareTracker.Helper
emailBody = emailBody.Replace("{TableBody}", tableBody);
try
{
foreach (string emailAddress in emailAddresses)
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
}
SendEmail(emailAddresses, emailSubject, emailBody);
return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) {
string to = emailTo;
private bool SendEmail(List<string> emailTo, string emailSubject, string emailBody) {
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
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
var message = new MimeMessage();
message.From.Add(new MailboxAddress(from, from));
foreach(string emailRecipient in emailTo)
{
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
message.To.Add(new MailboxAddress(emailRecipient, emailRecipient));
}
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.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 CsvHelper;
using System.Globalization;
namespace CarCareTracker.Helper
@@ -8,13 +9,13 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.3.1";
public static string VersionNumber = "1.3.6";
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
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)
{
switch (input)
@@ -287,5 +288,202 @@ namespace CarCareTracker.Helper
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
Copyright (c) 2023 Hargata Softworks
Copyright (c) 2024 Hargata Softworks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -2,6 +2,7 @@
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -28,7 +29,7 @@ namespace CarCareTracker.Logic
bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset);
List<UserData> GetAllUsers();
List<Token> GetAllTokens();
KeyValuePair<string, string> GetPKCEChallengeCode();
}
public class LoginLogic : ILoginLogic
{
@@ -439,6 +440,14 @@ namespace CarCareTracker.Logic
{
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)
{
bool result = false;

View File

@@ -27,6 +27,18 @@ namespace CarCareTracker.MapProfile
Map(m => m.Type).Name(["type"]);
Map(m => m.Priority).Name(["priority"]);
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 EmailFrom { get; set; }
public bool UseSSL { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }

View File

@@ -10,9 +10,18 @@
public string RedirectURL { get; set; }
public string Scope { get; set; }
public string State { get; set; }
public string CodeChallenge { get; set; }
public bool ValidateState { get; set; } = false;
public bool DisableRegularLogin { get; set; } = false;
public bool UsePKCE { get; set; } = false;
public string LogOutURL { get; set; } = "";
public string 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 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 PartQuantity { get; set; }
public string Tags { get; set; }
public Dictionary<string,string> ExtraFields {get;set;}
}
public class SupplyRecordExportModel
@@ -37,9 +38,9 @@
public string Cost { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
}
public class ServiceRecordExportModel
public class GenericRecordExportModel
{
public string Date { get; set; }
public string Odometer { get; set; }
@@ -47,6 +48,7 @@
public string Notes { get; set; }
public string Cost { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
}
public class OdometerRecordExportModel
{
@@ -55,6 +57,7 @@
public string Odometer { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
}
public class TaxRecordExportModel
{
@@ -63,6 +66,7 @@
public string Notes { get; set; }
public string Cost { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
}
public class GasRecordExportModel
{
@@ -75,6 +79,7 @@
public string MissedFuelUp { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
}
public class ReminderExportModel
{
@@ -82,6 +87,8 @@
public string Urgency { get; set; }
public string Metric { get; set; }
public string Notes { get; set; }
public string DueDate { get; set; }
public string DueOdometer { get; set; }
}
public class PlanRecordExportModel
{
@@ -93,6 +100,6 @@
public string Priority { get; set; }
public string Progress { 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 SoldPrice { get; set; }
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();

View File

@@ -10,6 +10,7 @@
public string LicensePlate { get; set; }
public string SoldDate { get; set; }
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
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
### Kubernetes Deployment
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
### Need Help?
[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)
## Dependencies
- Bootstrap
- LiteDB
- Npgsql
- Bootstrap-DatePicker
- SweetAlert2
- CsvHelper
- Chart.js
- Drawdown
- [Bootstrap](https://github.com/twbs/bootstrap)
- [LiteDB](https://github.com/mbdavid/litedb)
- [Npgsql](https://github.com/npgsql/npgsql)
- [Bootstrap-DatePicker](https://github.com/uxsolutions/bootstrap-datepicker)
- [SweetAlert2](https://github.com/sweetalert2/sweetalert2)
- [CsvHelper](https://github.com/JoshClose/CsvHelper)
- [Chart.js](https://github.com/chartjs/Chart.js)
- [Drawdown](https://github.com/adamvleggett/drawdown)
- [MailKit](https://github.com/jstedfast/MailKit)
## License
LubeLogger utilizes a dual-licensing model, see [License](/LICENSE) for more information
MIT
## Support
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.
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)

View File

@@ -40,6 +40,55 @@
No Params
</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="col-1">
GET
@@ -270,56 +319,22 @@
No Params(must be root user)
</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>
$('.copyable').on('click', function (e) {
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>
</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>
<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>
</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>
<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;
}
<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>
</div>
<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 class="lead">
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-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>
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>
<div class="d-flex justify-content-center">
<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">Chart.js</li>
<li class="list-group-item">Drawdown</li>
<li class="list-group-item">MailKit</li>
</ul>
</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-dialog modal-lg" role="document">
<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){
//remove thousands separator.
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
var thousandSeparator = decodeHTMLEntities("@numberFormat.NumberGroupSeparator");
var decimalSeparator = decodeHTMLEntities("@numberFormat.NumberDecimalSeparator");
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
if (input == "---") {
input = "0";
@@ -85,13 +85,32 @@
return parseFloat(input);
}
function globalFloatToString(input) {
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
var decimalSeparator = decodeHTMLEntities("@numberFormat.NumberDecimalSeparator");
input = input.replace(".", decimalSeparator);
return input;
}
function genericErrorMessage(){
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>
@await RenderSectionAsync("Scripts", required: false)
</head>

View File

@@ -28,7 +28,7 @@
@if (isNew)
{
<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>
@@ -89,10 +89,9 @@
}
<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">
<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>

View File

@@ -151,7 +151,7 @@
</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-content" id="collisionRecordModalContent">
</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 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-content" id="gasRecordModalContent">
</div>

View File

@@ -17,23 +17,23 @@
consumptionUnit = "kWh";
} else if (useUKMPG)
{
consumptionUnit = "liters";
consumptionUnit = @translator.Translate(userLanguage, "liters");
}
else
{
consumptionUnit = useMPG ? "gallons" : "liters";
consumptionUnit = useMPG ? @translator.Translate(userLanguage, "gallons") : @translator.Translate(userLanguage, "liters");
}
if (useHours)
{
distanceUnit = "hours";
distanceUnit = @translator.Translate(userLanguage, "hours");
}
else if (useUKMPG)
{
distanceUnit = "miles";
distanceUnit = @translator.Translate(userLanguage, "miles");
}
else
{
distanceUnit = useMPG ? "miles" : "kilometers";
distanceUnit = useMPG ? @translator.Translate(userLanguage, "miles") : @translator.Translate(userLanguage, "kilometers");
}
}
<div class="modal-header">
@@ -57,7 +57,7 @@
@if (isNew)
{
<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>
@@ -121,6 +121,7 @@
<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>
}
<div id="filesPendingUpload"></div>
</div>
</div>
</div>

View File

@@ -45,6 +45,7 @@
<br />
<small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
}
<div id="filesPendingUpload"></div>
</div>
<div class="col-12">
<label for="noteRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>

View File

@@ -61,7 +61,7 @@
</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-content" id="noteModalContent">
</div>

View File

@@ -23,14 +23,22 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<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>
<div class="input-group">
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading")" value="@(isNew ? "" : Model.Mileage)">
@if (isNew)
{
<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>
@@ -68,6 +76,7 @@
<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>
}
<div id="filesPendingUpload"></div>
</div>
</div>
</div>

View File

@@ -151,7 +151,7 @@
</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-content" id="odometerRecordModalContent">
</div>

View File

@@ -73,10 +73,9 @@
{
<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">
<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>

View File

@@ -65,10 +65,9 @@
{
<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">
<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>

View File

@@ -97,14 +97,14 @@
</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-content" id="planRecordModalContent">
</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-content" id="planRecordTemplateModalContent">
</div>

View File

@@ -21,7 +21,7 @@
<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>
<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>
</div>
<div class="input-group">
@@ -29,7 +29,7 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<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>
</div>
<div class="input-group">
@@ -37,12 +37,12 @@
@if (isNew)
{
<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 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>
</div>
<div class="d-grid"></div>
@@ -62,7 +62,7 @@
<label class="form-check-label" for="reminderIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label>
</div>
<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="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 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="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
</select>
<label for="reminderRecurringMonth">Month</label>
<select class="form-select" onchange="checkCustomMonthInterval()" id="reminderRecurringMonth" @(Model.IsRecurring ? "" : "disabled")>
<label for="reminderRecurringMonth">@translator.Translate(userLanguage, "Month")</label>
<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="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>

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>
}
@if (reminderRecord.Metric == ReminderMetric.Date)
{
<td class="col-2">@reminderRecord.Date.ToShortDateString()</td>
}
else if (reminderRecord.Metric == ReminderMetric.Odometer)
{
<td class="col-2">@reminderRecord.Mileage</td>
}
else
{
<td class="col-2">@reminderRecord.Metric</td>
}
<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}")">
@if (reminderRecord.Metric == ReminderMetric.Date)
{
@reminderRecord.Date.ToShortDateString()
}
else if (reminderRecord.Metric == ReminderMetric.Odometer)
{
@reminderRecord.Mileage
}
else
{
@reminderRecord.Metric
}
</span>
</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>
@if (hasRefresh)
@@ -118,4 +122,8 @@
<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="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="col-12">
<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)
{
<option value="@year">@year</option>
@@ -32,7 +32,7 @@
<div class="col-12 col-md-10">
<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">
Metrics
@translator.Translate(userLanguage, "Metrics")
</button>
<ul class="dropdown-menu" style="width:100%;">
<li class="dropdown-item">

View File

@@ -28,7 +28,7 @@
@if (isNew)
{
<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>
@@ -91,6 +91,7 @@
<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>
}
<div id="filesPendingUpload"></div>
</div>
</div>
</div>

View File

@@ -149,7 +149,7 @@
</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-content" id="serviceRecordModalContent">
</div>

View File

@@ -34,7 +34,7 @@
<div class="input-group">
<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">
<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>
@@ -77,6 +77,7 @@
<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>
}
<div id="filesPendingUpload"></div>
</div>
</div>
</div>

View File

@@ -167,7 +167,7 @@
</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-content" id="supplyRecordModalContent">
</div>

View File

@@ -110,7 +110,7 @@
var inStockQuantity = globalParseFloat(inStock.text());
var unitPrice = globalParseFloat(priceField.text());
//validation
if (isNaN(requestedQuantity) || requestedQuantity > inStockQuantity) {
if (isNaN(requestedQuantity) || requestedQuantity > inStockQuantity || requestedQuantity <= 0) {
textField.addClass("is-invalid");
hasError = true;
} else {
@@ -131,7 +131,7 @@
var parsedFloat = globalFloatToString(totalSum);
$("#supplySumLabel").text(`Total: ${parsedFloat}`);
}
$("#selectSuppliesButton").attr('disabled', (hasError || totalSum == 0));
$("#selectSuppliesButton").attr('disabled', (hasError || selectedSupplies.toArray().length == 0));
if (!hasError) {
return {
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">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
<div id="filesPendingUpload"></div>
</div>
</div>
</div>

View File

@@ -143,7 +143,7 @@
</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-content" id="taxRecordModalContent">
</div>

View File

@@ -28,7 +28,7 @@
@if (isNew)
{
<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>
@@ -89,10 +89,9 @@
}
<label for="upgradeRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="upgradeRecordFiles">
<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>

View File

@@ -109,7 +109,7 @@
<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 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-3 flex-grow-1 flex-shrink-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@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)'>
<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-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-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
@@ -150,7 +150,7 @@
</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-content" id="upgradeRecordModalContent">
</div>

View File

@@ -6,8 +6,8 @@
var userLanguage = userConfig.UserLanguage;
}
@model List<UploadedFiles>
<label>@translator.Translate(userLanguage, "Uploaded Documents")</label>
<ul class="list-group">
<label id="uploadedDocumentsLabel">@translator.Translate(userLanguage, "Uploaded Documents")</label>
<ul class="list-group" id="uploadedDocumentsList">
@foreach (UploadedFiles filesUploaded in Model)
{
<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>
}
else if (Model.VehicleData.IsDiesel)
{
<span><i class="bi bi-fuel-pump-diesel me-2"></i>@translator.Translate(userLanguage, "Diesel")</span>
}
else
{
<span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span>

View File

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

View File

@@ -30,6 +30,7 @@ services:
POSTGRES_PASSWORD: "lubepass"
POSTGRES_DB: "lubelogger"
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
- postgres:/var/lib/postgresql/data
- /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');
})
}
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() {
$('#accountInformationModal').modal('hide');
}

View File

@@ -182,7 +182,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -196,7 +196,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -213,7 +213,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -227,7 +227,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -244,7 +244,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -258,7 +258,7 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${globalFloatToString(convertedAmount.toFixed(decimalPoints))}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;

View File

@@ -208,4 +208,12 @@ function saveMultipleOdometerRecordsToVehicle() {
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() {
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
if (reminderIsRecurring) {
$("#reminderRecurringMileage").attr('disabled', false);
$("#reminderRecurringMonth").attr('disabled', false);
//check selected metric
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 {
$("#reminderRecurringMileage").attr('disabled', true);
$("#reminderRecurringMonth").attr('disabled', true);

View File

@@ -1,5 +1,5 @@
function getYear() {
return $("#yearOption").val();
return $("#yearOption").val() ?? '0';
}
function generateVehicleHistoryReport() {
var vehicleId = GetVehicleId().vehicleId;
@@ -54,7 +54,10 @@ function getSelectedMetrics() {
if (selectedMetricCheckBoxes.length == 6) {
$("#selectAllExpenseCheck").prop("checked", true);
}
$('#yearOption').val(yearMetric);
//check if option is available
if ($("#yearOption").has(`option[value=${yearMetric}]`).length > 0) {
$('#yearOption').val(yearMetric);
}
$("#reminderOption").val(reminderMetric);
//retrieve data.
yearUpdated();

View File

@@ -40,7 +40,8 @@ function saveVehicle(isEdit) {
var vehiclePurchaseDate = $("#inputPurchaseDate").val();
var vehicleSoldDate = $("#inputSoldDate").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 vehicleHasOdometerAdjustment = $("#inputHasOdometerAdjustment").is(':checked');
var vehicleOdometerMultiplier = $("#inputOdometerMultiplier").val();
@@ -119,6 +120,7 @@ function saveVehicle(isEdit) {
model: vehicleModel,
licensePlate: vehicleLicensePlate,
isElectric: vehicleIsElectric,
isDiesel: vehicleIsDiesel,
tags: vehicleTags,
useHours: vehicleUseHours,
extraFields: extraFields.extraFields,
@@ -178,8 +180,8 @@ function uploadFileAsync(event) {
});
}
function isValidMoney(input) {
const euRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}(\.?\d{3})?(,\d{1,3}?)?\)?$/;
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\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}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
return (euRegex.test(input) || usRegex.test(input));
}
function initDatePicker(input, futureOnly) {
@@ -339,7 +341,7 @@ function updateAggregateLabels() {
if (labelsToSum.length > 0) {
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
var sumDistanceLabel = $("[data-aggregate-type='sum-distance']");
@@ -373,8 +375,12 @@ function uploadVehicleFilesAsync(event) {
type: 'POST',
success: function (response) {
sloader.hide();
$(event).val(""); //clear out the filename from the uploader
if (response.length > 0) {
uploadedFiles.push.apply(uploadedFiles, response);
$.post('/Vehicle/GetFilesPendingUpload', { uploadedFiles: uploadedFiles }, function (viewData) {
$("#filesPendingUpload").html(viewData);
});
}
},
error: function () {
@@ -386,6 +392,15 @@ function uploadVehicleFilesAsync(event) {
function deleteFileFromUploadedFiles(fileLocation, event) {
event.parentElement.parentElement.parentElement.remove();
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) {
Swal.fire({
@@ -1053,4 +1068,24 @@ function bindModalInputChanges(modalName) {
$(`#${modalName} select, #${modalName} input[type='checkbox']`).off('input').on('input', function (e) {
$(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,9 +480,9 @@ function getRecordsDeltaStats(recordIds) {
var diffOdo = maxOdo - minOdo;
var diffDate = maxDate - minDate;
var divisibleCount = recordIds.length - 1;
var averageOdo = diffOdo > 0 ? (diffOdo / divisibleCount).toFixed(2) : 0;
var averageDays = diffDate > 0 ? Math.floor((diffDate / divisibleCount) / 8.64e7) : 0;
var averageSum = costSum > 0 ? (costSum / recordIds.length).toFixed(2) : 0;
var averageOdo = diffOdo > 0 ? (diffOdo / divisibleCount).toFixed(2) : '0';
var averageDays = diffDate > 0 ? Math.floor((diffDate / divisibleCount) / 8.64e7) : '0';
var averageSum = costSum > 0 ? (costSum / recordIds.length).toFixed(2) : '0';
costSum = costSum.toFixed(2);
Swal.fire({
title: "Record Statistics",
@@ -490,9 +490,9 @@ function getRecordsDeltaStats(recordIds) {
<br />
<p>Average Days between Records: ${averageDays}</p>
<br />
<p>Total Cost: ${getGlobalConfig().currencySymbol} ${globalFloatToString(costSum)}</p>
<p>Total Cost: ${globalAppendCurrency(globalFloatToString(costSum))}</p>
<br />
<p>Average Cost: ${getGlobalConfig().currencySymbol} ${globalFloatToString(averageSum)}</p>`
<p>Average Cost: ${globalAppendCurrency(globalFloatToString(averageSum))}</p>`
,
icon: "info"
});

View File

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

View File

@@ -6,22 +6,44 @@
{
"src": "/defaults/lubelogger_icon_72.png",
"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",
"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",
"sizes": "144x144",
"type": "image/png"
"type": "image/png",
"purpose": "any"
},
{
"src": "/defaults/lubelogger_icon_192.png",
"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": [

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