Compare commits

..

103 Commits

Author SHA1 Message Date
Hargata Softworks
6cf733b9c6 Merge pull request #558 from hargata/Hargata/545
enable extra fields to be imported via CSV
2024-07-09 14:52:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1eb6e2cedf enable extra fields to be imported via CSV 2024-07-09 14:48:09 -06:00
Hargata Softworks
ef4deaba8f Merge pull request #557 from hargata/Hargata/512
recurring interval are now based on reminder metric.
2024-07-09 11:16:09 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8c196c2fa recurring interval are now based on reminder metric. 2024-07-09 11:15:02 -06:00
Hargata Softworks
f7c9db6353 Merge pull request #556 from hargata/Hargata/mailconfig.cleanup
Clean up MailConfig and update npsql
2024-07-09 10:35:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4ce720ff97 Update npgSQL 2024-07-09 10:34:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a96011629b clean up mailconfig 2024-07-09 10:32:40 -06:00
Hargata Softworks
9dcdcf97e8 Merge pull request #508 from snpaul22/main
Create app schema automatically
2024-07-06 17:40:13 -06:00
snpaul22
5148338f52 Merge branch 'hargata:main' into main 2024-07-06 18:54:14 -04:00
Hargata Softworks
0d3c04d8f8 Merge pull request #554 from hargata/Hargata/root.user.update
Make it easier to update root user credentials and patch bug.
2024-07-05 11:26:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
15328a14b4 Make it easier to update root user credentials and patch bug. 2024-07-05 11:18:46 -06:00
Hargata Softworks
2d092f722a Merge pull request #550 from hargata/Hargata/disabled.init.odo
updated button color.
2024-07-02 15:05:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8825cb9b9b updated button color. 2024-07-02 15:05:16 -06:00
Hargata Softworks
2f17e303ab Merge pull request #549 from hargata/Hargata/disabled.init.odo
Disable the initial odometer reading field
2024-07-02 11:51:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4b56c8a343 a better implementation of disabled attribute 2024-07-02 11:09:41 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7b6b62c623 Disable the initial odometer reading field if it's non-zero to prevent accidental editing. 2024-07-02 10:54:11 -06:00
Hargata Softworks
07f5e66491 Merge pull request #519 from NateWright/main
Add maskable icons for Android PWA
2024-06-18 08:25:49 -06:00
Hargata Softworks
fe633f3220 Merge pull request #542 from kapcake/patch-1
Update money regex to allow more than 6 figures on integer part
2024-06-18 08:25:00 -06:00
kapcake
af1090553f Update money regex to not allow unlimited figures
The regex now allows up to 8 groups of 3 digits on the integer part, which is within the bounds of C# decimal, preventing the user to go out of bounds on the form input.
2024-06-18 15:14:26 +02:00
kapcake
92c2e66660 Update money regex to allow more than 6 figures on integer part 2024-06-18 15:02:20 +02:00
Nathan Wright
08372f9dcb Merge branch 'hargata:main' into main 2024-06-08 15:20:31 -04:00
Hargata Softworks
64ea0e2eee Merge pull request #533 from hargata/Hargata/532
add support for smtp clients that requires no authentication.
2024-05-31 09:11:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7ab476a88f add support for smtp clients that requires no authentication. 2024-05-31 09:11:09 -06:00
Hargata Softworks
de41ca911d Merge pull request #530 from hargata/Hargata/527
Minor Bug Fixes
2024-05-29 09:36:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dde9688f96 Updated supplies usage validation to allow for free supplies. 2024-05-28 14:54:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c1ca63edc0 removed unnecessary tostring 2024-05-28 14:26:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cbc430499f added new currency formatting function 2024-05-28 14:14:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4da9fa4802 See 514 2024-05-28 13:55:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
42586d9556 Updated version number 2024-05-28 12:56:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ea4387d4ab Add missing translation keys 2024-05-28 12:53:20 -06:00
Hargata Softworks
0707b515ab Merge pull request #525 from hargata/Hargata/sponsor.tiers
Updated Sponsors File Path
2024-05-21 15:42:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78ae71fc46 Updated Sponsors File Path 2024-05-21 15:41:05 -06:00
Hargata Softworks
3f62cd40e7 Merge pull request #524 from hargata/Hargata/sponsor.tiers
Add Sponsor Section
2024-05-21 15:32:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
47657c0093 Added sponsor section. 2024-05-21 15:29:46 -06:00
Hargata Softworks
a5b0fde4b6 Merge pull request #522 from hargata/Hargata/swal.uncensored
Hargata/swal.uncensored
2024-05-20 12:26:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78cc0b34b1 Updated version number 2024-05-20 12:25:07 -06:00
Hargata Softworks
e3017e986b Merge pull request #520 from hargata/Hargata/license.change
Removed commercial license restrictions from license.
2024-05-20 12:22:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e12cd876db removed unused files 2024-05-20 12:21:54 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5292e4b814 utilize uncensored version of sweet alert 2024-05-20 12:16:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dbdd16ab89 Removed commercial license restrictions from license. 2024-05-14 08:46:47 -06:00
nwright
61c2600286 added maskable icons 2024-05-13 19:34:03 -04:00
snpaul22
163a33ae3a Create init.sql
Script checks for "app" schema in Postgres during startup. It will create the schema automatically if it doesn't exist or skip if it does exist.
2024-05-04 11:27:50 -04:00
snpaul22
37d064aa62 Update docker-compose.postgresql.yml
Added Postgres volume bind for initialization script
2024-05-04 11:23:55 -04:00
Hargata Softworks
ddc3c2e1b5 Merge pull request #503 from hargata/Hargata/null.mail.config
null check for mail config
2024-04-25 12:45:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
49184b287b null check for mail config 2024-04-25 12:45:21 -06:00
Hargata Softworks
f6139bda0d Merge pull request #502 from hargata/Hargata/mailkit.upgrade
fix inefficiencies
2024-04-25 11:33:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a6471b823b fix inefficiencies 2024-04-25 11:11:54 -06:00
Hargata Softworks
7c34003647 Merge pull request #501 from hargata/Hargata/mailkit.upgrade
MailKit Upgrade
2024-04-25 10:46:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1aa21f9980 Uprgade from .NET SMTPClient to MailKit as the default smtpclient does not support modern protocols. 2024-04-25 10:45:55 -06:00
Hargata Softworks
ce4ca50939 Merge pull request #499 from hargata/Hargata/persist.metric.bug
fix getyear method
2024-04-23 23:50:31 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fb28260c4a fix getyear method 2024-04-23 23:50:06 -06:00
Hargata Softworks
626a904747 Merge pull request #498 from hargata/Hargata/persist.metric.bug
Minor bug fix
2024-04-23 23:43:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
893cdafdc5 Fixes a very minor bug where the persisted year metric blanks out when viewing a different car that doesn't have that specific year. 2024-04-23 23:42:09 -06:00
Hargata Softworks
dbfb7d7d9c Merge pull request #493 from hargata/Hargata/persist.dashboard
check against null instead of undefined,
2024-04-15 08:42:43 -06:00
DESKTOP-GENO133\IvanPlex
a66538a7db check against null instead of undefined, 2024-04-15 08:41:53 -06:00
Hargata Softworks
2f77d87d4f Merge pull request #492 from hargata/Hargata/persist.dashboard
temporarily persist dashboard metrics in sessionStorage
2024-04-15 08:25:54 -06:00
DESKTOP-GENO133\IvanPlex
de85ba984c temporarily persist dashboard metrics in sessionStorage 2024-04-15 08:20:37 -06:00
Hargata Softworks
caac1a05ae Merge pull request #480 from hargata/Hargata/fix.link.color
fix donation link colors
2024-04-12 08:04:44 -06:00
Hargata Softworks
eb5793b819 Merge pull request #490 from hargata/Hargata/fix.alt.fuel
Fix Alternate Fuel Mileage Bug
2024-04-12 08:04:28 -06:00
DESKTOP-GENO133\IvanPlex
5ef3e1e2ce updated version number 2024-04-12 08:04:04 -06:00
DESKTOP-GENO133\IvanPlex
d8b459e5ee persist regional formatting when viewing record statistics. 2024-04-12 08:01:25 -06:00
DESKTOP-GENO133\IvanPlex
7b40d58aa1 fix alternate fuel mileage auto converting to NA decimal format bug. 2024-04-12 07:54:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
809e9b838e fix donation link colors 2024-04-09 21:29:19 -06:00
Hargata Softworks
23ae36ebd9 Merge pull request #475 from hargata/Hargata/update.readme
check if function exists.
2024-04-07 21:03:31 -06:00
DESKTOP-GENO133\IvanPlex
9c3f7d20f5 fix plans not updating when deleted. 2024-04-07 21:01:06 -06:00
DESKTOP-GENO133\IvanPlex
083298303c check if function exists. 2024-04-07 20:51:55 -06:00
Hargata Softworks
224970a07e Merge pull request #474 from hargata/Hargata/update.readme
Fix the problem with time.
2024-04-07 19:15:41 -06:00
DESKTOP-GENO133\IvanPlex
86d039e5b0 Fix the problem with time. 2024-04-07 17:59:08 -06:00
Hargata Softworks
6a8038aac9 Merge pull request #473 from hargata/Hargata/update.readme
update readme
2024-04-07 13:59:02 -06:00
DESKTOP-GENO133\IvanPlex
e748f08a8e update readme 2024-04-07 13:58:32 -06:00
Hargata Softworks
3580963e9f Merge pull request #472 from hargata/Hargata/planner.improvement.part2
Improved UI UX For Planner Edit
2024-04-07 12:51:28 -06:00
DESKTOP-GENO133\IvanPlex
b008ce2ab8 Improved UI UX 2024-04-07 12:50:28 -06:00
Hargata Softworks
ea0c2c7061 Merge pull request #471 from hargata/Hargata/transl
Updated translation
2024-04-07 11:26:35 -06:00
DESKTOP-GENO133\IvanPlex
0926220933 Updated translation 2024-04-07 11:26:12 -06:00
Hargata Softworks
ca975bbdd3 Merge pull request #470 from hargata/Hargata/planner.template.edit
updated translation
2024-04-07 11:22:20 -06:00
DESKTOP-GENO133\IvanPlex
b16c5c5302 updated translation 2024-04-07 11:21:43 -06:00
Hargata Softworks
cad05fe5d9 Merge pull request #466 from hargata/Hargata/planner.improvement
Moved template modal outside of add modal.
2024-04-07 11:20:42 -06:00
Hargata Softworks
a7cd466d9c Merge pull request #469 from hargata/Hargata/planner.template.edit
Add functionality to let users edit plan record templates
2024-04-07 11:20:28 -06:00
DESKTOP-GENO133\IvanPlex
4472a67ec0 fixed label function 2024-04-07 11:19:52 -06:00
DESKTOP-GENO133\IvanPlex
c84a4029ec basic functionality to edit templates 2024-04-07 11:09:05 -06:00
DESKTOP-GENO133\IvanPlex
d7d9ab505e Add functionality to let users edit plan record templates 2024-04-07 08:31:17 -06:00
DESKTOP-GENO133\IvanPlex
c582f5f5c7 Moved template modal outside of add modal. 2024-04-05 19:36:26 -06:00
Hargata Softworks
4c30939339 Merge pull request #465 from hargata/Hargata/unused.translation.key
Updated translation file and unsaved changes label color.
2024-04-05 18:50:02 -06:00
DESKTOP-GENO133\IvanPlex
cecd6a1d2b Updated translation file and unsaved changes label color. 2024-04-05 18:48:54 -06:00
Hargata Softworks
853dcbb364 Merge pull request #464 from hargata/Hargata/unused.translation.key
removed cached translation key
2024-04-05 07:18:08 -06:00
DESKTOP-GENO133\IvanPlex
ae327ed26d removed cached translation key 2024-04-05 07:17:49 -06:00
DESKTOP-GENO133\IvanPlex
3e416aa255 Revert "removed unused translation key"
This reverts commit ab98d02106faceaca1c6c76b8f0de7bf3e28cebe.
2024-04-05 07:16:11 -06:00
DESKTOP-GENO133\IvanPlex
61c2c3fc83 removed unused translation key 2024-04-05 07:16:10 -06:00
Hargata Softworks
ca749aaf1e Merge pull request #463 from hargata/Hargata/unsaved.changes
updated cached view to only show when there are unsaved changes present.
2024-04-05 07:11:50 -06:00
DESKTOP-GENO133\IvanPlex
2a2cb3bd0c updated cached view to only show when there are unsaved changes present. 2024-04-05 07:10:09 -06:00
Hargata Softworks
44e3d19844 Merge pull request #462 from hargata/Hargata/global.search
added incremental search
2024-04-04 19:16:22 -06:00
DESKTOP-GENO133\IvanPlex
1e25fffc70 added incremental search 2024-04-04 19:14:49 -06:00
Hargata Softworks
44645ed23d Merge pull request #461 from hargata/Hargata/global.search
added global search
2024-04-04 10:48:57 -06:00
DESKTOP-GENO133\IvanPlex
fa557e5f76 added global search 2024-04-04 10:46:43 -06:00
Hargata Softworks
7202fda38e Merge pull request #458 from hargata/Hargata/cached.records
Cached Record.
2024-04-02 21:53:46 -06:00
DESKTOP-GENO133\IvanPlex
d05afe41d6 Cache the record the user was viewing if they didn't save, delete, or move it. 2024-04-02 21:51:02 -06:00
Hargata Softworks
bfc0b58728 Merge pull request #453 from hargata/Hargata/bring.back.modal
Allow users to bring back modal window they accidentally closed.
2024-04-02 07:39:25 -06:00
DESKTOP-GENO133\IvanPlex
22e8aaca81 Allow users to bring back modal window they accidentally closed. 2024-04-02 07:29:16 -06:00
Hargata Softworks
5cb1247fb2 Merge pull request #452 from hargata/Hargata/odometer.increment
Odometer Increments
2024-04-01 18:31:27 -06:00
DESKTOP-GENO133\IvanPlex
17a6d99703 Allow users to increment for last reported odometer reading when creating new records. 2024-04-01 18:25:20 -06:00
Hargata Softworks
3e7917f767 Create FUNDING.yml 2024-04-01 09:14:46 -06:00
Hargata Softworks
e9277d4dd9 Merge pull request #443 from hargata/Hargata/reminder.icons
reminders will now display urgency in icons instead of a full badge w…
2024-03-29 09:35:17 -06:00
DESKTOP-GENO133\IvanPlex
058edd8af6 reminders will now display urgency in icons instead of a full badge when viewed in small screens. 2024-03-29 08:15:55 -06:00
68 changed files with 1280 additions and 10666 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=""

14
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: lubelogger
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

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

@@ -105,7 +105,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 +114,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 +219,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

@@ -220,7 +220,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 +235,7 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[Authorize]
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult DestroyLoginCreds()
{

View File

@@ -9,6 +9,7 @@ using CarCareTracker.MapProfile;
using System.Security.Claims;
using CarCareTracker.Logic;
using CarCareTracker.Filter;
using System.Text.Json;
namespace CarCareTracker.Controllers
{
@@ -438,7 +439,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 }).ToList() : new List<ExtraField>()
};
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{
@@ -494,7 +496,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 }).ToList() : new List<ExtraField>()
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -517,7 +520,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 }).ToList() : new List<ExtraField>()
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
}
@@ -536,7 +540,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 }).ToList() : new List<ExtraField>()
};
_planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord);
}
@@ -550,7 +555,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 }).ToList() : new List<ExtraField>()
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -574,7 +580,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 }).ToList() : new List<ExtraField>()
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -600,7 +607,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 }).ToList() : new List<ExtraField>()
};
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord);
}
@@ -613,7 +621,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 }).ToList() : new List<ExtraField>()
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
}
@@ -1152,6 +1161,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++)
@@ -1912,6 +1925,33 @@ namespace CarCareTracker.Controllers
}
return PartialView("_SupplyRecords", result);
}
[HttpGet]
public IActionResult GetSupplyRecordsForPlanRecordTemplate(int planRecordTemplateId)
{
var viewModel = new SupplyUsageViewModel();
var planRecordTemplate = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (planRecordTemplate != default && planRecordTemplate.VehicleId != default)
{
var supplies = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(planRecordTemplate.VehicleId);
if (_config.GetServerEnableShopSupplies())
{
supplies.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
}
supplies.RemoveAll(x => x.Quantity <= 0);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
supplies = supplies.OrderByDescending(x => x.Date).ToList();
}
else
{
supplies = supplies.OrderBy(x => x.Date).ToList();
}
viewModel.Supplies = supplies;
viewModel.Usage = planRecordTemplate.Supplies;
}
return PartialView("_SupplyUsage", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetSupplyRecordsForRecordsByVehicleId(int vehicleId)
@@ -1931,7 +1971,11 @@ namespace CarCareTracker.Controllers
{
result = result.OrderBy(x => x.Date).ToList();
}
return PartialView("_SupplyUsage", result);
var viewModel = new SupplyUsageViewModel
{
Supplies = result
};
return PartialView("_SupplyUsage", viewModel);
}
[HttpPost]
public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord)
@@ -2023,15 +2067,11 @@ namespace CarCareTracker.Controllers
{
//check if template name already taken.
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
if (existingRecord)
if (planRecord.Id == default && existingRecord)
{
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle" });
}
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (planRecord.Supplies.Any() && planRecord.CopySuppliesAttachment)
{
planRecord.Files.AddRange(GetSuppliesAttachments(planRecord.Supplies));
}
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
return Json(new OperationResponse { Success = result, Message = result ? "Template Added" : StaticHelper.GenericErrorMessage });
}
@@ -2081,6 +2121,10 @@ namespace CarCareTracker.Controllers
if (existingRecord.Supplies.Any())
{
existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description);
if (existingRecord.CopySuppliesAttachment)
{
existingRecord.Files.AddRange(GetSuppliesAttachments(existingRecord.Supplies));
}
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
@@ -2113,7 +2157,7 @@ namespace CarCareTracker.Controllers
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Now,
Date = DateTime.Now.Date,
VehicleId = existingRecord.VehicleId,
Mileage = odometer,
Notes = $"Auto Insert From Plan Record: {existingRecord.Description}",
@@ -2126,7 +2170,7 @@ namespace CarCareTracker.Controllers
var newRecord = new ServiceRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
@@ -2142,7 +2186,7 @@ namespace CarCareTracker.Controllers
var newRecord = new CollisionRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
@@ -2158,7 +2202,7 @@ namespace CarCareTracker.Controllers
var newRecord = new UpgradeRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
@@ -2178,6 +2222,12 @@ namespace CarCareTracker.Controllers
return Json(result);
}
[HttpGet]
public IActionResult GetPlanRecordTemplateForEditById(int planRecordTemplateId)
{
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
return PartialView("_PlanRecordTemplateEditModal", result);
}
[HttpGet]
public IActionResult GetPlanRecordForEditById(int planRecordId)
{
var result = _planRecordDataAccess.GetPlanRecordById(planRecordId);
@@ -2340,6 +2390,89 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Shared Methods"
[HttpPost]
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult SearchRecords(int vehicleId, string searchQuery)
{
List<SearchResult> searchResults = new List<SearchResult>();
if (string.IsNullOrWhiteSpace(searchQuery))
{
return Json(searchResults);
}
foreach(ImportMode visibleTab in _config.GetUserConfig(User).VisibleTabs)
{
switch (visibleTab)
{
case ImportMode.ServiceRecord:
{
var results = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ServiceRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.RepairRecord:
{
var results = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.RepairRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.UpgradeRecord:
{
var results = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.UpgradeRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.TaxRecord:
{
var results = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.TaxRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.SupplyRecord:
{
var results = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.SupplyRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.PlanRecord:
{
var results = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.PlanRecord, Description = $"{x.DateCreated.ToShortDateString()} - {x.Description}" }));
}
break;
case ImportMode.OdometerRecord:
{
var results = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.OdometerRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" }));
}
break;
case ImportMode.GasRecord:
{
var results = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.GasRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" }));
}
break;
case ImportMode.NoteRecord:
{
var results = _noteDataAccess.GetNotesByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.NoteRecord, Description = $"{x.Description}" }));
}
break;
case ImportMode.ReminderRecord:
{
var results = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ReminderRecord, Description = $"{x.Description}" }));
}
break;
}
}
return PartialView("_GlobalSearchResult", searchResults);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetMaxMileage(int vehicleId)
{
var result = _vehicleLogic.GetMaxMileage(vehicleId);
return Json(result);
}
public IActionResult MoveRecord(int recordId, ImportMode source, ImportMode destination)
{
var genericRecord = new GenericRecord();

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,44 +119,49 @@ 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);
var message = new MimeMessage();
message.From.Add(new MailboxAddress(from, from));
foreach(string emailRecipient in emailTo)
{
message.To.Add(new MailboxAddress(emailRecipient, emailRecipient));
}
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);
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);
}
try
{
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
}
else
{
client.Send(message);
}
client.Disconnect(true);
return true;
}
catch (Exception ex)
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}
}

View File

@@ -8,13 +8,13 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.2.9";
public static string VersionNumber = "1.3.5";
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)
@@ -259,5 +259,33 @@ namespace CarCareTracker.Helper
};
httpClient.PostAsJsonAsync(webhookURL, httpParams);
}
public static string GetImportModeIcon(ImportMode importMode)
{
switch (importMode)
{
case ImportMode.ServiceRecord:
return "bi-card-checklist";
case ImportMode.RepairRecord:
return "bi-exclamation-octagon";
case ImportMode.UpgradeRecord:
return "bi-wrench-adjustable";
case ImportMode.TaxRecord:
return "bi-currency-dollar";
case ImportMode.SupplyRecord:
return "bi-shop";
case ImportMode.PlanRecord:
return "bi-bar-chart-steps";
case ImportMode.OdometerRecord:
return "bi-speedometer";
case ImportMode.GasRecord:
return "bi-fuel-pump";
case ImportMode.NoteRecord:
return "bi-journal-bookmark";
case ImportMode.ReminderRecord:
return "bi-bell";
default:
return "bi-file-bar-graph";
}
}
}
}

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

@@ -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; }

9
Models/SearchResult.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class SearchResult
{
public int Id { get; set; }
public ImportMode RecordType { get; set; }
public string Description { get; set; }
}
}

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

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

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class SupplyUsageViewModel
{
public List<SupplyRecord> Supplies { get; set; } = new List<SupplyRecord>();
public List<SupplyUsage> Usage { get; set; } = new List<SupplyUsage>();
}
}

View File

@@ -22,15 +22,6 @@ 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
### Docker Setup (Manual Build for Advanced Users)
1. Install Docker
2. Clone this repo
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up`
### Need Help?
[Documentation](https://docs.lubelogger.com/)
@@ -39,19 +30,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.

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-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
If you want to support the development of this project, consider subscribing to <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
</p>
<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

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Repair Record") : translator.Translate(userLanguage,"Edit Repair Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Repair Record") : translator.Translate(userLanguage, "Edit Repair Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditCollisionRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddCollisionRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -23,7 +23,15 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="collisionRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" 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('collisionRecordMileage')"><i class="bi bi-plus"></i></button>
</div>
}
</div>
<label for="collisionRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) repaired(i.e. Alternator)")" value="@Model.Description">
@if (isNew)
@@ -81,7 +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>
</div>

View File

@@ -17,27 +17,27 @@
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">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Gas Record") : translator.Translate(userLanguage,"Edit Gas Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Gas Record") : translator.Translate(userLanguage, "Edit Gas Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditGasRecordModal({Model.GasRecord.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -52,7 +52,15 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="gasRecordMileage">@($"{translator.Translate(userLanguage,"Odometer Reading")}({distanceUnit})")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
@if (isNew)
{
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('gasRecordMileage')"><i class="bi bi-plus"></i></button>
</div>
}
</div>
<label for="gasRecordGallons">@($"{translator.Translate(userLanguage, "Fuel Consumption")}({consumptionUnit})")</label>
<input type="text" inputmode="decimal" id="gasRecordGallons" class="form-control" placeholder="@translator.Translate(userLanguage,"Amount of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Gallons)">
<div class="form-check form-switch">

View File

@@ -0,0 +1,34 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<SearchResult>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div style="max-height:50vh; overflow-y:auto;">
@if (Model.Any())
{
@foreach (SearchResult result in Model)
{
<div class="row border p-2 m-1" onclick="loadGlobalSearchResult(@result.Id, '@result.RecordType')" style="cursor:pointer;">
<div class="col-1">
<i class="bi @StaticHelper.GetImportModeIcon(result.RecordType)"></i>
</div>
<div class="col-11">
@result.Description
</div>
</div>
}
} else
{
<div class="row border p-2 m-1">
<div class="col-1">
<i class="bi bi-ban"></i>
</div>
<div class="col-11">
@translator.Translate(userLanguage, "No Data Found")
</div>
</div>
}
</div>

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Note") : translator.Translate(userLanguage, "Edit Note"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Note") : translator.Translate(userLanguage, "Edit Note"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditNoteModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddNoteModal()" aria-label="Close"></button>
</div>
<div class="modal-body">

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Odometer Record") : translator.Translate(userLanguage,"Edit Odometer Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Odometer Record") : translator.Translate(userLanguage, "Edit Odometer Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditOdometerRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddOdometerRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -23,9 +23,25 @@
<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')"><i class="bi bi-plus"></i></button>
</div>
}
</div>
<label for="odometerRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="odometerRecordTag">
@foreach (string tag in Model.Tags)

View File

@@ -1,5 +1,5 @@
@model PlanRecord
<div class="taskCard @(Model.Progress == PlanProgress.Done ? "nodrag" : "") text-dark user-select-none mt-2 mb-2" draggable="@(Model.Progress == PlanProgress.Done ? "false" : "true")" ondragstart="dragStart(event, @Model.Id)" onclick="@(Model.Progress == PlanProgress.Done ? $"deletePlanRecord({Model.Id})" : $"showEditPlanRecordModal({Model.Id})")">
<div class="taskCard @(Model.Progress == PlanProgress.Done ? "nodrag" : "") text-dark user-select-none mt-2 mb-2" draggable="@(Model.Progress == PlanProgress.Done ? "false" : "true")" ondragstart="dragStart(event, @Model.Id)" onclick="@(Model.Progress == PlanProgress.Done ? $"deletePlanRecord({Model.Id}, true)" : $"showEditPlanRecordModal({Model.Id})")">
<div class="card-body">
<div class="row">
<div class="col-12 col-lg-8 text-truncate">

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Plan Record") : translator.Translate(userLanguage, "Edit Plan Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Plan Record") : translator.Translate(userLanguage, "Edit Plan Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditPlanRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddPlanRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -101,10 +101,6 @@
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="savePlanRecordTemplate()">@translator.Translate(userLanguage, "Save as Template")</a></li>
@if (!Model.CreatedFromReminder)
{
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
}
</ul>
</div>
}
@@ -130,7 +126,8 @@
id: @Model.Id,
dateCreated: decodeHTMLEntities('@(Model.DateCreated)'),
reminderRecordId: decodeHTMLEntities('@Model.ReminderRecordId'),
createdFromReminder: @Model.CreatedFromReminder.ToString().ToLower()
createdFromReminder: @Model.CreatedFromReminder.ToString().ToLower(),
isTemplate: false
}
}
</script>

View File

@@ -0,0 +1,117 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model PlanRecordInput
@{
var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@translator.Translate(userLanguage, "Edit Plan Record Template")<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditPlanRecordTemplateModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddPlanRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="planRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="planRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage, "Describe the Plan")" value="@Model.Description">
<label for="planRecordCost">@translator.Translate(userLanguage, "Cost")</label>
<input type="text" inputmode="decimal" id="planRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage, "Cost of the Plan")" value="@Model.Cost">
@await Html.PartialAsync("_SupplyStore", "PlanRecordTemplate")
<label for="planRecordType">@translator.Translate(userLanguage, "Type")</label>
<select class="form-select" id="planRecordType">
<!option value="ServiceRecord" @(Model.ImportMode == ImportMode.ServiceRecord || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Service")</!option>
<!option value="RepairRecord" @(Model.ImportMode == ImportMode.RepairRecord ? "selected" : "")>@translator.Translate(userLanguage, "Repair")</!option>
<!option value="UpgradeRecord" @(Model.ImportMode == ImportMode.UpgradeRecord ? "selected" : "")>@translator.Translate(userLanguage, "Upgrade")</!option>
</select>
<label for="planRecordPriority">@translator.Translate(userLanguage, "Priority")</label>
<select class="form-select" id="planRecordPriority">
<!option value="Critical" @(Model.Priority == PlanPriority.Critical ? "selected" : "")>@translator.Translate(userLanguage, "Critical")</!option>
<!option value="Normal" @(Model.Priority == PlanPriority.Normal || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Normal")</!option>
<!option value="Low" @(Model.Priority == PlanPriority.Low ? "selected" : "")>@translator.Translate(userLanguage, "Low")</!option>
</select>
<label for="planRecordProgress">@translator.Translate(userLanguage, "Current Stage")</label>
<select class="form-select" id="planRecordProgress">
<!option value = "Backlog" @(Model.Progress == PlanProgress.Backlog || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Planned")</!option>
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select>
@foreach (ExtraField field in Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div>
<div class="col-md-6 col-12">
<label for="planRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="planRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="planRecordFiles">@translator.Translate(userLanguage, "Upload more documents")</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>
</div>
}
else
{
<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>
}
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
}
<button type="button" class="btn btn-danger" onclick="deletePlannerRecordTemplate(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddPlanRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" class="btn btn-primary" onclick="savePlanRecordTemplate(true)">@translator.Translate(userLanguage, "Edit Plan Record Template")</button>
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
var selectedSupplies = [];
var copySuppliesAttachments = @Model.CopySuppliesAttachment.ToString().ToLower();
getUploadedFilesFromModel();
getSelectedSuppliesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
function getSelectedSuppliesFromModel() {
@foreach(SupplyUsage supplyUsage in Model.Supplies)
{
@:selectedSupplies.push({supplyId: @supplyUsage.SupplyId, quantity: @supplyUsage.Quantity})
}
}
function getPlanRecordModelData() {
return {
id: @Model.Id,
dateCreated: decodeHTMLEntities('@(Model.DateCreated)'),
reminderRecordId: decodeHTMLEntities('@Model.ReminderRecordId'),
createdFromReminder: @Model.CreatedFromReminder.ToString().ToLower(),
isTemplate: true
}
}
</script>

View File

@@ -20,7 +20,7 @@
<tr class="d-flex">
<th scope="col" class="col-8">@translator.Translate(userLanguage,"Description")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Use")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Edit")</th>
</tr>
</thead>
<tbody>
@@ -67,7 +67,7 @@
}
</td>
<td class="col-2"><button type="button" class="btn btn-primary" onclick="usePlannerRecordTemplate(@planRecordTemplate.Id)"><i class="bi bi-plus-square"></i></button></td>
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deletePlannerRecordTemplate(@planRecordTemplate.Id)"><i class="bi bi-trash"></i></button></td>
<td class="col-2"><button type="button" class="btn btn-warning" onclick="showEditPlanRecordTemplateModal(@planRecordTemplate.Id)"><i class="bi bi-pencil-square"></i></button></td>
</tr>
}
</tbody>

View File

@@ -28,6 +28,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('PlanRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('PlanRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
</ul>
</div>
}
@@ -102,7 +104,7 @@
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="planRecordTemplateModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal fade" data-bs-focus="false" id="planRecordTemplateModal" tabindex="-1" role="dialog" aria-hidden="true">
<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,17 +29,20 @@
<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">
<input type="number" inputmode="numeric" id="reminderMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Future 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="appendMileageToOdometer(500)">+500</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>
@@ -59,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>
@@ -80,7 +83,7 @@
<!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")>
<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

@@ -59,19 +59,19 @@
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@reminderRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditReminderRecordModal,@reminderRecord.Id)" data-tags='@string.Join(" ", reminderRecord.Tags)'>
@if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent)
{
<td class="col-1"><span class="badge text-bg-danger">@translator.Translate(userLanguage, "Very Urgent")</span></td>
<td class="col-1"><span class="text-danger d-inline-block d-md-none"><i class="bi bi-hourglass-split h3"></i></span><span class="badge text-bg-danger d-none d-md-inline-block">@translator.Translate(userLanguage, "Very Urgent")</span></td>
}
else if (reminderRecord.Urgency == ReminderUrgency.Urgent)
{
<td class="col-1"><span class="badge text-bg-warning">@translator.Translate(userLanguage, "Urgent")</span></td>
<td class="col-1"><span class="text-warning d-inline-block d-md-none"><i class="bi bi-hourglass-split h3"></i></span><span class="badge text-bg-warning d-none d-md-inline-block">@translator.Translate(userLanguage, "Urgent")</span></td>
}
else if (reminderRecord.Urgency == ReminderUrgency.PastDue)
{
<td class="col-1"><span class="badge text-bg-secondary">@translator.Translate(userLanguage, "Past Due")</span></td>
<td class="col-1"><span class="text-secondary d-inline-block d-md-none"><i class="bi bi-hourglass-bottom h3"></i></span><span class="badge text-bg-secondary d-none d-md-inline-block">@translator.Translate(userLanguage, "Past Due")</span></td>
}
else
{
<td class="col-1"><span class="badge text-bg-success">@translator.Translate(userLanguage, "Not Urgent")</span></td>
<td class="col-1"><span class="text-success d-inline-block d-md-none"><i class="bi bi-hourglass-top h3"></i></span><span class="badge text-bg-success d-none d-md-inline-block">@translator.Translate(userLanguage, "Not Urgent")</span></td>
}
@if (reminderRecord.Metric == ReminderMetric.Date)
{

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">
@@ -117,6 +117,9 @@
</div>
</div>
<div class="col-md-3 col-12 chartContainer">
<div class="d-grid">
<button onclick="showGlobalSearch()" class="btn btn-secondary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Search")<i class="bi ms-2 bi-search"></i></button>
</div>
<div class="d-grid">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Vehicle Maintenance Report")<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
@@ -126,4 +129,25 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="globalSearchModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="globalSearchModalContent">
<div class="modal-body">
<div class="input-group input-group-lg">
<input type="text" id="globalSearchInput" autocomplete="off" onkeyup="handleGlobalSearchKeyPress(event)" class="form-control" placeholder="@translator.Translate(userLanguage,"Search by Keyword(Case Sensitive)")">
<button type="button" class="btn btn-outline-secondary" onclick="performGlobalSearch()"><i class="bi bi-search"></i></button>
</div>
<div class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" role="switch" id="globalSearchAutoSearchCheck" checked>
<label class="form-check-label" for="globalSearchAutoSearchCheck">@translator.Translate(userLanguage, "Incremental Search")</label>
</div>
<div id="globalSearchModalResults"></div>
</div>
</div>
</div>
</div>
<div id="vehicleHistoryReport" class="showOnPrint"></div>
<script>
getSelectedMetrics();
</script>

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Service Record") : translator.Translate(userLanguage,"Edit Service Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Service Record") : translator.Translate(userLanguage, "Edit Service Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditServiceRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddServiceRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -23,7 +23,15 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="serviceRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" 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('serviceRecordMileage')"><i class="bi bi-plus"></i></button>
</div>
}
</div>
<label for="serviceRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="serviceRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) serviced(i.e. Oil Change)")" value="@Model.Description">
@if (isNew)

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Supply Record") : translator.Translate(userLanguage,"Edit Supply Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Supply Record") : translator.Translate(userLanguage, "Edit Supply Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditSupplyRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddSupplyRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -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>

View File

@@ -33,6 +33,7 @@
$('#upgradeRecordCost').val(selectedSupplyResult.totalSum);
break;
case "PlanRecord":
case "PlanRecordTemplate":
$('#planRecordCost').val(selectedSupplyResult.totalSum);
break;
}
@@ -53,6 +54,7 @@
$('#upgradeRecordModal').modal('hide');
break;
case "PlanRecord":
case "PlanRecordTemplate":
$('#planRecordModal').modal('hide');
break;
}
@@ -70,6 +72,7 @@
$('#upgradeRecordModal').modal('show');
break;
case "PlanRecord":
case "PlanRecordTemplate":
$('#planRecordModal').modal('show');
break;
}
@@ -83,6 +86,21 @@
}
}
function getSupplies() {
var caller = GetCaller().tab;
if (caller == 'PlanRecordTemplate') {
var planRecordTemplateId = getPlanRecordModelData().id;
$.get(`/Vehicle/GetSupplyRecordsForPlanRecordTemplate?planRecordTemplateId=${planRecordTemplateId}`, function (data) {
if (data) {
hideParentModal();
$("#inputSuppliesModalContent").html(data);
$('#inputSuppliesModal').modal('show');
recalculateTotal();
if (copySuppliesAttachments) {
$('#inputCopySuppliesAttachments').attr('checked', true);
}
}
});
} else {
var vehicleId = GetVehicleId().vehicleId;
$.get(`/Vehicle/GetSupplyRecordsForRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
@@ -90,7 +108,8 @@
$("#inputSuppliesModalContent").html(data);
$('#inputSuppliesModal').modal('show');
}
})
});
}
}
function hideSuppliesModal() {
$('#inputSuppliesModal').modal('hide');

View File

@@ -4,15 +4,15 @@
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var recordTags = Model.Supplies.SelectMany(x => x.Tags).Distinct();
}
@model List<SupplyRecord>
@model SupplyUsageViewModel
<div class="modal-header">
<h5 class="modal-title">@translator.Translate(userLanguage,"Select Supplies")</h5>
<button type="button" class="btn-close" onclick="hideSuppliesModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@if (Model.Any())
@if (Model.Supplies.Any())
{
<div class="row">
<div class="col-12" style="max-height:50vh; overflow-y:auto;" id="supplies-table">
@@ -37,11 +37,12 @@
</tr>
</thead>
<tbody>
@foreach (SupplyRecord supplyRecord in Model)
@foreach (SupplyRecord supplyRecord in Model.Supplies)
{
var supplyUsage = Model.Usage.Where(x => x.SupplyId == supplyRecord.Id).SingleOrDefault();
<tr class="d-flex" id="supplyRows" data-tags='@string.Join(" ", supplyRecord.Tags)'>
<td class="col-1"><input class="form-check-input" type="checkbox" onchange="toggleQuantityFieldDisabled(this)" value="@supplyRecord.Id"></td>
<td class="col-2"><input type="text" inputmode="decimal" disabled onchange="recalculateTotal()" class="form-control"></td>
<td class="col-1"><input class="form-check-input" type="checkbox" onchange="toggleQuantityFieldDisabled(this)" value="@supplyRecord.Id" @(supplyUsage == default ? "" : "checked")></td>
<td class="col-2"><input type="text" inputmode="decimal" @(supplyUsage == default ? "disabled" : "") value="@(supplyUsage == default ? "" : supplyUsage.Quantity)" onchange="recalculateTotal()" class="form-control"></td>
<td class="col-2 supplyquantity">@supplyRecord.Quantity</td>
<td class="col-2 text-truncate">@StaticHelper.TruncateStrings(supplyRecord.PartNumber)</td>
<td class="col-3 text-truncate">@StaticHelper.TruncateStrings(supplyRecord.Description)</td>
@@ -109,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 {
@@ -130,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

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Tax Record") : translator.Translate(userLanguage,"Edit Tax Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Tax Record") : translator.Translate(userLanguage, "Edit Tax Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditTaxRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddTaxRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Upgrade Record") : translator.Translate(userLanguage,"Edit Upgrade Record"))</h5>
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Upgrade Record") : translator.Translate(userLanguage, "Edit Upgrade Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditUpgradeRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
<button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -23,7 +23,15 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="upgradeRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" 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('upgradeRecordMileage')"><i class="bi bi-plus"></i></button>
</div>
}
</div>
<label for="upgradeRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) upgraded/modded")" value="@Model.Description">
@if (isNew)
@@ -81,7 +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>
</div>

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

@@ -9,7 +9,19 @@
}
});
}
function showEditCollisionRecordModal(collisionRecordId) {
function showEditCollisionRecordModal(collisionRecordId, nocache) {
if (!nocache) {
var existingContent = $("#collisionRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getCollisionRecordModelData().id;
if (existingId == collisionRecordId && $('[data-changed=true]').length > 0) {
$('#collisionRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetCollisionRecordForEditById?collisionRecordId=${collisionRecordId}`, function (data) {
if (data) {
$("#collisionRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditCollisionRecordModal(collisionRecordId) {
initDatePicker($('#collisionRecordDate'));
initTagSelector($("#collisionRecordTag"));
$('#collisionRecordModal').modal('show');
bindModalInputChanges('collisionRecordModal');
$('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("collisionRecordNotes");

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

@@ -9,7 +9,19 @@
}
});
}
function showEditGasRecordModal(gasRecordId) {
function showEditGasRecordModal(gasRecordId, nocache) {
if (!nocache) {
var existingContent = $("#gasRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getGasRecordModelData().id;
if (existingId == gasRecordId && $('[data-changed=true]').length > 0) {
$('#gasRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetGasRecordForEditById?gasRecordId=${gasRecordId}`, function (data) {
if (data) {
$("#gasRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditGasRecordModal(gasRecordId) {
initDatePicker($('#gasRecordDate'));
initTagSelector($("#gasRecordTag"));
$('#gasRecordModal').modal('show');
bindModalInputChanges('gasRecordModal');
$('#gasRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("gasRecordNotes");
@@ -162,28 +175,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "l":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
sender.attr("data-unit", "l");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
case "imp gal":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
sender.attr("data-unit", "imp gal");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -193,28 +206,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "US gal":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
sender.attr("data-unit", "US gal");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
case "imp gal":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
sender.attr("data-unit", "imp gal");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -224,28 +237,28 @@ function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
case "US gal":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
sender.attr("data-unit", "US gal");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
case "l":
$("[data-gas-type='consumption']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
sender.attr("data-unit", "l");
});
$("[data-gas-type='unitcost']").map((index, elem) => {
var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`;
});
if (save) { setDebounce(saveUserGasTabPreferences); }
break;
@@ -262,7 +275,7 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
var convertedAmount = globalParseFloat(elem.innerText);
if (convertedAmount > 0) {
convertedAmount = 100 / convertedAmount;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
}
});
//update labels up top.
@@ -270,19 +283,19 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
if (newAverage > 0) {
newAverage = 100 / newAverage;
var averageLabel = $("#averageFuelMileageLabel");
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${newAverage.toFixed(2)}`);
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(newAverage.toFixed(2))}`);
}
var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim());
if (newMin > 0) {
newMin = 100 / newMin;
var minLabel = $("#minFuelMileageLabel");
minLabel.text(`${minLabel.text().split(':')[0]}: ${newMin.toFixed(2)}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(newMin.toFixed(2))}`);
}
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim());
if (newMax > 0) {
newMax = 100 / newMax;
var maxLabel = $("#maxFuelMileageLabel");
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${newMax.toFixed(2)}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(newMax.toFixed(2))}`);
}
sender.text(sender.text().replace(sender.attr("data-unit"), "km/l"));
sender.attr("data-unit", "km/l");
@@ -296,27 +309,26 @@ function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
var convertedAmount = globalParseFloat(elem.innerText);
if (convertedAmount > 0) {
convertedAmount = 100 / convertedAmount;
elem.innerText = convertedAmount.toFixed(2);
elem.innerText = globalFloatToString(convertedAmount.toFixed(2));
}
});
var newAverage = globalParseFloat($("#averageFuelMileageLabel").text().split(":")[1].trim());
if (newAverage > 0) {
newAverage = 100 / newAverage;
var averageLabel = $("#averageFuelMileageLabel");
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${newAverage.toFixed(2)}`);
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(newAverage.toFixed(2))}`);
}
var newMin = globalParseFloat($("#minFuelMileageLabel").text().split(":")[1].trim());
if (newMin > 0) {
newMin = 100 / newMin;
var minLabel = $("#minFuelMileageLabel");
minLabel.text(`${minLabel.text().split(':')[0]}: ${newMin.toFixed(2)}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(newMin.toFixed(2))}`);
}
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().split(":")[1].trim());
if (newMax > 0) {
newMax = 100 / newMax;
var maxLabel = $("#maxFuelMileageLabel");
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${newMax.toFixed(2)}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(newMax.toFixed(2))}`);
}
sender.text(sender.text().replace(sender.attr("data-unit"), "l/100km"));
sender.attr("data-unit", "l/100km");
@@ -345,17 +357,17 @@ function updateMPGLabels() {
if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l' && averageMPG > 0) {
averageMPG = 100 / averageMPG;
}
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${averageMPG.toFixed(2)}`);
averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(averageMPG.toFixed(2))}`);
} else {
averageLabel.text(`${averageLabel.text().split(':')[0]}: 0.00`);
}
if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l') {
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${minMPG.toFixed(2)}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${maxMPG.toFixed(2)}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`);
}
else {
minLabel.text(`${minLabel.text().split(':')[0]}: ${minMPG.toFixed(2)}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${maxMPG.toFixed(2)}`);
minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`);
maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`);
}
}
}

View File

@@ -7,12 +7,25 @@
}
});
}
function showEditNoteModal(noteId) {
function showEditNoteModal(noteId, nocache) {
if (!nocache) {
var existingContent = $("#noteModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getNoteModelData().id;
if (existingId == noteId && $('[data-changed=true]').length > 0) {
$('#noteModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) {
if (data) {
$("#noteModalContent").html(data);
initTagSelector($("#noteRecordTag"));
$('#noteModal').modal('show');
bindModalInputChanges('noteModal');
$('#noteModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("noteTextArea");

View File

@@ -9,7 +9,19 @@
}
});
}
function showEditOdometerRecordModal(odometerRecordId) {
function showEditOdometerRecordModal(odometerRecordId, nocache) {
if (!nocache) {
var existingContent = $("#odometerRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getOdometerRecordModelData().id;
if (existingId == odometerRecordId && $('[data-changed=true]').length > 0) {
$('#odometerRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetOdometerRecordForEditById?odometerRecordId=${odometerRecordId}`, function (data) {
if (data) {
$("#odometerRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditOdometerRecordModal(odometerRecordId) {
initDatePicker($('#odometerRecordDate'));
initTagSelector($("#odometerRecordTag"));
$('#odometerRecordModal').modal('show');
bindModalInputChanges('odometerRecordModal');
$('#odometerRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("odometerRecordNotes");
@@ -196,3 +209,11 @@ function saveMultipleOdometerRecordsToVehicle() {
}
})
}
function toggleInitialOdometerEnabled() {
if ($("#initialOdometerRecordMileage").prop("disabled")) {
$("#initialOdometerRecordMileage").prop("disabled", false);
} else {
$("#initialOdometerRecordMileage").prop("disabled", true);
}
}

View File

@@ -8,13 +8,57 @@
}
});
}
function showEditPlanRecordModal(planRecordId) {
function showEditPlanRecordModal(planRecordId, nocache) {
if (!nocache) {
var existingContent = $("#planRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getPlanRecordModelData().id;
var isNotTemplate = !getPlanRecordModelData().isTemplate;
if (existingId == planRecordId && isNotTemplate && $('[data-changed=true]').length > 0) {
$('#planRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetPlanRecordForEditById?planRecordId=${planRecordId}`, function (data) {
if (data) {
$("#planRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#planRecordDate'));
$('#planRecordModal').modal('show');
bindModalInputChanges('planRecordModal');
$('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("planRecordNotes");
}
});
}
});
}
function showEditPlanRecordTemplateModal(planRecordTemplateId, nocache) {
hidePlanRecordTemplatesModal();
if (!nocache) {
var existingContent = $("#planRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getPlanRecordModelData().id;
var isTemplate = getPlanRecordModelData().isTemplate;
if (existingId == planRecordTemplateId && isTemplate && $('[data-changed=true]').length > 0) {
$('#planRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetPlanRecordTemplateForEditById?planRecordTemplateId=${planRecordTemplateId}`, function (data) {
if (data) {
$("#planRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#planRecordDate'));
$('#planRecordModal').modal('show');
bindModalInputChanges('planRecordModal');
$('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("planRecordNotes");
@@ -29,8 +73,11 @@ function hideAddPlanRecordModal() {
//show reminder Modal
$("#reminderRecordModal").modal("show");
}
if (getPlanRecordModelData().isTemplate) {
showPlanRecordTemplatesModal();
}
function deletePlanRecord(planRecordId) {
}
function deletePlanRecord(planRecordId, noModal) {
$("#workAroundInput").show();
Swal.fire({
title: "Confirm Deletion?",
@@ -42,7 +89,9 @@ function deletePlanRecord(planRecordId) {
if (result.isConfirmed) {
$.post(`/Vehicle/DeletePlanRecordById?planRecordId=${planRecordId}`, function (data) {
if (data) {
if (!noModal) {
hideAddPlanRecordModal();
}
successToast("Plan Record Deleted");
var vehicleId = GetVehicleId().vehicleId;
getVehiclePlanRecords(vehicleId);
@@ -85,22 +134,19 @@ function showPlanRecordTemplatesModal() {
$.get(`/Vehicle/GetPlanRecordTemplatesForVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#planRecordTemplateModalContent").html(data);
hideAddPlanRecordModal();
$('#planRecordTemplateModal').modal('show');
}
});
}
function hidePlanRecordTemplatesModal() {
$('#planRecordTemplateModal').modal('hide');
$('#planRecordModal').modal('show');
}
function usePlannerRecordTemplate(planRecordTemplateId) {
$.post(`/Vehicle/ConvertPlanRecordTemplateToPlanRecord?planRecordTemplateId=${planRecordTemplateId}`, function (data) {
if (data.success) {
var vehicleId = GetVehicleId().vehicleId;
successToast(data.message);
$('#planRecordTemplateModal').modal('hide');
hideAddPlanRecordModal();
hidePlanRecordTemplatesModal();
saveScrollPosition();
getVehiclePlanRecords(vehicleId);
} else {
@@ -122,8 +168,8 @@ function deletePlannerRecordTemplate(planRecordTemplateId) {
$.post(`/Vehicle/DeletePlanRecordTemplateById?planRecordTemplateId=${planRecordTemplateId}`, function (data) {
$("#workAroundInput").hide();
if (data) {
successToast("Template Deleted");
hidePlanRecordTemplatesModal();
successToast("Plan Template Deleted");
hideAddPlanRecordModal();
} else {
errorToast(genericErrorMessage());
}
@@ -133,7 +179,7 @@ function deletePlannerRecordTemplate(planRecordTemplateId) {
}
});
}
function savePlanRecordTemplate() {
function savePlanRecordTemplate(isEdit) {
//get values
var formValues = getAndValidatePlanRecordValues();
//validate
@@ -144,7 +190,14 @@ function savePlanRecordTemplate() {
//save to db.
$.post('/Vehicle/SavePlanRecordTemplateToVehicleId', { planRecord: formValues }, function (data) {
if (data.success) {
successToast(data.message);
if (isEdit) {
hideAddPlanRecordModal();
showPlanRecordTemplatesModal();
$('[data-changed=true]').attr('data-changed', false)
successToast('Plan Template Updated');
} else {
successToast('Plan Template Added');
}
} else {
errorToast(data.message);
}

View File

@@ -137,8 +137,20 @@ function appendMileageToOdometer(increment) {
function enableRecurring() {
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
if (reminderIsRecurring) {
$("#reminderRecurringMileage").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;
@@ -29,6 +29,43 @@ function refreshMPGChart() {
$("#monthFuelMileageReportContent").html(data);
})
}
function setSelectedMetrics() {
var selectedMetricCheckBoxes = [];
$(".reportCheckBox:checked").map((index, elem) => {
selectedMetricCheckBoxes.push(elem.id);
});
var yearMetric = $('#yearOption').val();
var reminderMetric = $("#reminderOption").val();
sessionStorage.setItem("selectedMetricCheckBoxes", JSON.stringify(selectedMetricCheckBoxes));
sessionStorage.setItem("yearMetric", yearMetric);
sessionStorage.setItem("reminderMetric", reminderMetric);
}
function getSelectedMetrics() {
var selectedMetricCheckBoxes = sessionStorage.getItem("selectedMetricCheckBoxes");
var yearMetric = sessionStorage.getItem("yearMetric");
var reminderMetric = sessionStorage.getItem("reminderMetric");
if (selectedMetricCheckBoxes != null && yearMetric != null && reminderMetric != null) {
selectedMetricCheckBoxes = JSON.parse(selectedMetricCheckBoxes);
$(".reportCheckBox").prop('checked', false);
$("#selectAllExpenseCheck").prop("checked", false);
selectedMetricCheckBoxes.map(x => {
$(`#${x}`).prop('checked', true);
});
if (selectedMetricCheckBoxes.length == 6) {
$("#selectAllExpenseCheck").prop("checked", true);
}
//check if option is available
if ($("#yearOption").has(`option[value=${yearMetric}]`).length > 0) {
$('#yearOption').val(yearMetric);
}
$("#reminderOption").val(reminderMetric);
//retrieve data.
yearUpdated();
updateReminderPie();
return true;
}
return false;
}
function refreshBarChart() {
var selectedMetrics = [];
var vehicleId = GetVehicleId().vehicleId;
@@ -62,10 +99,12 @@ function refreshBarChart() {
$("#gasCostByMonthReportContent").html(data);
refreshMPGChart();
});
setSelectedMetrics();
}
function updateReminderPie() {
var vehicleId = GetVehicleId().vehicleId;
var daysToAdd = $("#reminderOption").val();
setSelectedMetrics();
$.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) {
$("#reminderMakeUpReportContent").html(data);
});
@@ -141,3 +180,73 @@ function exportAttachments() {
}
});
}
function showGlobalSearch() {
$('#globalSearchModal').modal('show');
}
function hideGlobalSearch() {
$('#globalSearchModal').modal('hide');
}
function performGlobalSearch() {
var searchQuery = $('#globalSearchInput').val();
if (searchQuery.trim() == '') {
$('#globalSearchInput').addClass('is-invalid');
} else {
$('#globalSearchInput').removeClass('is-invalid');
}
$.post('/Vehicle/SearchRecords', { vehicleId: GetVehicleId().vehicleId, searchQuery: searchQuery }, function (data) {
$('#globalSearchModalResults').html(data);
});
}
function handleGlobalSearchKeyPress(event) {
if ($('#globalSearchAutoSearchCheck').is(':checked')){
setDebounce(performGlobalSearch);
} else if (event.keyCode == 13) {
performGlobalSearch();
}
}
function loadGlobalSearchResult(recordId, recordType) {
hideGlobalSearch();
switch (recordType) {
case "ServiceRecord":
$('#servicerecord-tab').tab('show');
waitForElement('#serviceRecordModalContent', showEditServiceRecordModal, recordId);
break;
case "RepairRecord":
$('#accident-tab').tab('show');
waitForElement('#collisionRecordModalContent', showEditCollisionRecordModal, recordId);
break;
case "UpgradeRecord":
$('#upgrade-tab').tab('show');
waitForElement('#upgradeRecordModalContent', showEditUpgradeRecordModal, recordId);
break;
case "TaxRecord":
$('#tax-tab').tab('show');
waitForElement('#taxRecordModalContent', showEditTaxRecordModal, recordId);
break;
case "SupplyRecord":
$('#supply-tab').tab('show');
waitForElement('#supplyRecordModalContent', showEditSupplyRecordModal, recordId);
break;
case "NoteRecord":
$('#notes-tab').tab('show');
waitForElement('#noteModalContent', showEditNoteModal, recordId);
break;
case "OdometerRecord":
$('#odometer-tab').tab('show');
waitForElement('#odometerRecordModalContent', showEditOdometerRecordModal, recordId);
break;
case "ReminderRecord":
$('#reminder-tab').tab('show');
waitForElement('#reminderRecordModalContent', showEditReminderRecordModal, recordId);
break;
case "GasRecord":
$('#gas-tab').tab('show');
waitForElement('#gasRecordModalContent', showEditGasRecordModal, recordId);
break;
case "PlanRecord":
$('#plan-tab').tab('show');
waitForElement('#planRecordModalContent', showEditPlanRecordModal, recordId);
break;
}
}

View File

@@ -9,7 +9,19 @@
}
});
}
function showEditServiceRecordModal(serviceRecordId) {
function showEditServiceRecordModal(serviceRecordId, nocache) {
if (!nocache) {
var existingContent = $("#serviceRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getServiceRecordModelData().id;
if (existingId == serviceRecordId && $('[data-changed=true]').length > 0) {
$('#serviceRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetServiceRecordForEditById?serviceRecordId=${serviceRecordId}`, function (data) {
if (data) {
$("#serviceRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditServiceRecordModal(serviceRecordId) {
initDatePicker($('#serviceRecordDate'));
initTagSelector($("#serviceRecordTag"));
$('#serviceRecordModal').modal('show');
bindModalInputChanges('serviceRecordModal');
$('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("serviceRecordNotes");

View File

@@ -178,8 +178,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 +339,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']");
@@ -708,7 +708,10 @@ $(window).on('keydown', function (e) {
selectAllRows();
}
}
})
});
function getCurrentTab() {
return $(".tab-pane.active.show").attr('id');
}
function selectAllRows() {
clearSelectedRows();
$('.vehicleDetailTabContainer .table tbody tr:visible').addClass('table-active');
@@ -1032,3 +1035,22 @@ function copyToClipboard(e) {
function noPropagation() {
event.stopPropagation();
}
var checkExist;
function waitForElement(element, callBack, callBackParameter) {
checkExist = setInterval(function () {
if ($(`${element}`).length) {
callBack(callBackParameter);
clearInterval(checkExist);
}
}, 100); // check every 100ms
}
function bindModalInputChanges(modalName) {
//bind text inputs
$(`#${modalName} input[type='text'], #${modalName} input[type='number'], #${modalName} textarea`).off('input').on('input', function (e) {
$(e.currentTarget).attr('data-changed', true);
});
$(`#${modalName} select, #${modalName} input[type='checkbox']`).off('input').on('input', function (e) {
$(e.currentTarget).attr('data-changed', true);
});
}

View File

@@ -9,7 +9,19 @@
}
});
}
function showEditSupplyRecordModal(supplyRecordId) {
function showEditSupplyRecordModal(supplyRecordId, nocache) {
if (!nocache) {
var existingContent = $("#supplyRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getSupplyRecordModelData().id;
if (existingId == supplyRecordId && $('[data-changed=true]').length > 0) {
$('#supplyRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetSupplyRecordForEditById?supplyRecordId=${supplyRecordId}`, function (data) {
if (data) {
$("#supplyRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditSupplyRecordModal(supplyRecordId) {
initDatePicker($('#supplyRecordDate'));
initTagSelector($("#supplyRecordTag"));
$('#supplyRecordModal').modal('show');
bindModalInputChanges('supplyRecordModal');
$('#supplyRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("supplyRecordNotes");

View File

@@ -9,7 +9,19 @@
}
});
}
function showEditTaxRecordModal(taxRecordId) {
function showEditTaxRecordModal(taxRecordId, nocache) {
if (!nocache) {
var existingContent = $("#taxRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getTaxRecordModelData().id;
if (existingId == taxRecordId && $('[data-changed=true]').length > 0) {
$('#taxRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetTaxRecordForEditById?taxRecordId=${taxRecordId}`, function (data) {
if (data) {
$("#taxRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditTaxRecordModal(taxRecordId) {
initDatePicker($('#taxRecordDate'));
initTagSelector($("#taxRecordTag"));
$('#taxRecordModal').modal('show');
bindModalInputChanges('taxRecordModal');
$('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("taxRecordNotes");

View File

@@ -9,7 +9,19 @@
}
});
}
function showEditUpgradeRecordModal(upgradeRecordId) {
function showEditUpgradeRecordModal(upgradeRecordId, nocache) {
if (!nocache) {
var existingContent = $("#upgradeRecordModalContent").html();
if (existingContent.trim() != '') {
//check if id is same.
var existingId = getUpgradeRecordModelData().id;
if (existingId == upgradeRecordId && $('[data-changed=true]').length > 0) {
$('#upgradeRecordModal').modal('show');
$('.cached-banner').show();
return;
}
}
}
$.get(`/Vehicle/GetUpgradeRecordForEditById?upgradeRecordId=${upgradeRecordId}`, function (data) {
if (data) {
$("#upgradeRecordModalContent").html(data);
@@ -17,6 +29,7 @@ function showEditUpgradeRecordModal(upgradeRecordId) {
initDatePicker($('#upgradeRecordDate'));
initTagSelector($("#upgradeRecordTag"));
$('#upgradeRecordModal').modal('show');
bindModalInputChanges('upgradeRecordModal');
$('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("upgradeRecordNotes");

View File

@@ -75,6 +75,10 @@ $(document).ready(function () {
$("#odometer-tab-pane").html("");
break;
}
$(`.lubelogger-tab #${e.target.id}`).addClass('active');
$(`.lubelogger-mobile-nav #${e.target.id}`).addClass('active');
$(`.lubelogger-tab #${e.relatedTarget.id}`).removeClass('active');
$(`.lubelogger-mobile-nav #${e.relatedTarget.id}`).removeClass('active');
});
var defaultTab = GetDefaultTab().tab;
switch (defaultTab) {
@@ -476,19 +480,19 @@ 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",
html: `<p>Average Distance Traveled between Records: ${averageOdo}</p>
html: `<p>Average Distance Traveled between Records: ${globalFloatToString(averageOdo)}</p>
<br />
<p>Average Days between Records: ${averageDays}</p>
<br />
<p>Total Cost: ${getGlobalConfig().currencySymbol} ${costSum}</p>
<p>Total Cost: ${globalAppendCurrency(globalFloatToString(costSum))}</p>
<br />
<p>Average Cost: ${getGlobalConfig().currencySymbol} ${averageSum}</p>`
<p>Average Cost: ${globalAppendCurrency(globalFloatToString(averageSum))}</p>`
,
icon: "info"
});
@@ -608,3 +612,37 @@ function getAndValidateSelectedRecurringReminder() {
}
}
}
function getLastOdometerReadingAndIncrement(odometerFieldName) {
Swal.fire({
title: 'Increment Last Reported Odometer Reading',
html: `
<input type="text" id="inputOdometerIncrement" class="swal2-input" placeholder="Increment">
`,
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const odometerIncrement = parseInt(globalParseFloat($("#inputOdometerIncrement").val()));
if (isNaN(odometerIncrement) || odometerIncrement <= 0) {
Swal.showValidationMessage(`Please enter a non-zero amount to increment`);
}
return { odometerIncrement }
},
}).then(function (result) {
if (result.isConfirmed) {
var amountToIncrement = result.value.odometerIncrement;
$.get(`/Vehicle/GetMaxMileage?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
var newAmount = data + amountToIncrement;
if (!isNaN(newAmount)) {
var odometerField = $(`#${odometerFieldName}`);
if (odometerField.length > 0) {
odometerField.val(newAmount);
} else {
errorToast(genericErrorMessage());
}
} else {
errorToast(genericErrorMessage());
}
});
}
});
}

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