Compare commits

...

304 Commits

Author SHA1 Message Date
Hargata Softworks
79633dbe1d Merge pull request #799 from hargata/Hargata/796
Update configurator
2025-01-10 12:13:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f325306e20 Update configurator 2025-01-10 12:13:34 -07:00
Hargata Softworks
4778656063 Merge pull request #798 from hargata/Hargata/796
fix mileage not included.
2025-01-10 11:02:35 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f7e00523c2 fix mileage not included. 2025-01-10 11:01:24 -07:00
Hargata Softworks
bf3df7230b Merge pull request #797 from hargata/Hargata/796
Hargata/796
2025-01-10 10:03:51 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c5182e0ed6 lol 2025-01-10 10:03:09 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4081438cba Defer calculations if odometer not provided. 2025-01-10 10:01:16 -07:00
Hargata Softworks
b72fe2bf37 Merge pull request #793 from hargata/Hargata/warn.dir.diff
Warn when working directory differs from content path.
2025-01-09 10:02:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4d9c687709 Warn when working directory differs from content path. 2025-01-09 10:02:14 -07:00
Hargata Softworks
792f295c45 Merge pull request #791 from hargata/Hargata/769
Fixed backup restoration for userConfig.json
2025-01-08 20:38:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
af28753558 use a static string instead. 2025-01-08 20:37:45 -07:00
DESKTOP-T0O5CDB\DESK-555BD
24fb663599 Fixed backup restoration for userConfig.json 2025-01-08 20:33:38 -07:00
Hargata Softworks
140506c9c3 Merge pull request #790 from hargata/Hargata/769
moved userConfig file into data folder.
2025-01-08 20:26:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
519f159c8c moved userConfig file into data folder. 2025-01-08 20:25:17 -07:00
Hargata Softworks
bb019cbcd9 Merge pull request #789 from hargata/Hargata/769
Fix gas record files attachment
2025-01-08 16:19:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
84219627ff Fix gas record files attachment 2025-01-08 16:18:36 -07:00
Hargata Softworks
f218e878c6 Merge pull request #788 from hargata/Hargata/769
Document Upload Endpoint
2025-01-08 15:55:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
520f47955b cleaned up usings. 2025-01-08 14:15:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8110ee18f1 Create endpoint to upload documents and add ability to attach uploaded documents to records. 2025-01-08 14:14:23 -07:00
Hargata Softworks
55bf817310 Merge pull request #782 from hargata/Hargata/697
added API method that generates a calendar of reminders for the user.
2025-01-08 12:39:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f2f55f8118 Restore favicon, previously was served up automatically by browser but now we want to link it explicitly. also brought back the security features for accessing static files. 2025-01-08 12:38:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f48e7cd0d4 update Controller Action Name, 2025-01-08 11:11:29 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c82e0c8b9b Use a MD5 hash to get exactly 16 bytes so the GUID is always valid and identical for the calendar event with same date and description. 2025-01-08 10:49:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c72877e16b Merge branch 'main' into Hargata/697 2025-01-08 10:40:10 -07:00
Hargata Softworks
ccc9076397 Merge pull request #786 from hargata/Hargata/785
move files out of webrootpath.
2025-01-08 10:13:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1c716ecf4a bump version 2025-01-08 10:12:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2cc471b944 move files out of webrootpath. 2025-01-08 10:10:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a6c450109a added API method that generates a calendar of reminders for the user. 2025-01-07 08:43:25 -07:00
Hargata Softworks
e13707305b Merge pull request #776 from hargata/Hargata/745
Fix annoying bug on macOS devices where context menu hides as soon as…
2024-12-31 09:21:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
68bc0383f4 Fix annoying bug on macOS devices where context menu hides as soon as it shows up. 2024-12-31 09:21:05 -07:00
Hargata Softworks
878f4f3687 Merge pull request #771 from hargata/Hargata/745
added bottom padding for garage view.
2024-12-16 17:32:20 -07:00
DESKTOP-T0O5CDB\DESK-555BD
48a721adda added bottom padding for garage view. 2024-12-16 17:31:30 -07:00
Hargata Softworks
548e8da78c Merge pull request #747 from hargata/Hargata/745
1.4.2 Changes
2024-12-16 09:45:01 -07:00
Hargata Softworks
beb28214cf Merge pull request #767 from hargata/Hargata/758
allow users to create odometer records from existing records.
2024-12-13 11:37:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b4bb31491b allow users to create odometer records from existing records. 2024-12-13 11:31:48 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0b5f007327 Styling fixes. 2024-12-13 10:59:53 -07:00
Hargata Softworks
d31a4aeea3 Merge pull request #766 from hargata/Hargata/761
Add Date Range Filter to Vehicle Maintenance Report
2024-12-12 11:58:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
97acd873eb Added date range filter. 2024-12-12 11:56:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5cf3538be3 garage view enhancement for mobile devices. 2024-12-11 19:19:36 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a34921701d webhook payload update. 2024-12-11 14:40:36 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5fcf54de09 cleaned up some code. 2024-12-11 12:32:40 -07:00
Hargata Softworks
68372bf995 Merge pull request #764 from hargata/Hargata/763
Hargata/763
2024-12-11 12:29:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9f99ac1531 Add tags to be exported. 2024-12-11 12:28:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b7384fda6b Spacing and translation keys. 2024-12-07 22:34:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8f4d610825 add spacing in report parameter. 2024-12-07 22:25:28 -07:00
Hargata Softworks
6bbf4b2575 Merge pull request #760 from hargata/Hargata/572
Hargata/572
2024-12-07 12:03:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
32db884620 Allow users to filter records by tags in the consolidated report. 2024-12-07 11:50:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1b736b36f8 Added delete records. 2024-12-06 11:34:04 -07:00
Hargata Softworks
22dfe5eb04 Merge pull request #756 from hargata/Hargata/574
Created nicer machine payloads for webhooks.
2024-12-06 08:43:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8fd49e20d5 added support for discord. 2024-12-06 08:42:25 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c791070126 Fixed webhook stuff. 2024-12-06 08:11:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
807603fd4f Created nicer machine payloads for webhooks. 2024-12-05 19:08:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a944e5e5ab See #744 2024-12-05 08:10:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
33e916cac2 force a page refresh if there are oudated recurring tax records on vehicle load. 2024-12-04 21:11:58 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a99d71dc41 pattern 2024-12-04 11:54:43 -07:00
DESKTOP-T0O5CDB\DESK-555BD
67d91f1a0d See 751 2024-12-04 11:38:17 -07:00
DESKTOP-T0O5CDB\DESK-555BD
de2db7d24e added security checks 2024-12-04 09:05:49 -07:00
Hargata Softworks
b360f77ea0 Merge pull request #754 from hargata/Hargata/752.2
Fixed naming policy.
2024-12-04 08:54:43 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e869528522 Fixed naming policy. 2024-12-04 08:15:46 -07:00
Hargata Softworks
7c0317d232 Merge pull request #753 from hargata/Hargata/752
Allow for JSON Body in POST/PUT API Methods
2024-12-03 14:15:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0e49497da1 added invariant exports to get methods. 2024-12-03 13:40:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0380382c47 allow API methods to receive JSON with rich datatypes. 2024-12-03 11:35:11 -07:00
Hargata Softworks
23201b8c86 Merge pull request #748 from hargata/Hargata/541
Added PUT API Methods
2024-11-30 12:25:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5f357c9f84 Fix CollaboratorFilter bug. 2024-11-30 12:23:48 -07:00
DESKTOP-T0O5CDB\DESK-555BD
244272621c bump version 2024-11-30 10:19:51 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5792b84880 Added PUT API Methods 2024-11-30 10:17:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8e644093f9 added try catch 2024-11-29 13:47:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9421cb57ca Create past records for outdated tax records. 2024-11-29 13:37:37 -07:00
Hargata Softworks
bd7cbffe10 Merge pull request #741 from hargata/Hargata/60
SimplyAuto Import
2024-11-25 09:27:24 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b293882f75 add more column aliases. 2024-11-25 09:26:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0984b2049b Added support for SimplyAuto Fuel Import. 2024-11-25 09:18:10 -07:00
Hargata Softworks
d4ec4989e7 Merge pull request #740 from hargata/Hargata/739
Refactored code in ConfigHelper.
2024-11-25 08:42:28 -07:00
DESKTOP-T0O5CDB\DESK-555BD
230433f784 Refactored a few more methods that rely on the IConfiguration object to use IConfigHelper instead. 2024-11-25 08:40:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d4dda481be pattern. 2024-11-25 08:18:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1d3b94f544 Refactored code in ConfigHelper. 2024-11-25 08:14:56 -07:00
Hargata Softworks
78434f0016 Merge pull request #737 from hargata/Hargata/overhaul.reminder.tab
Reminder Record Tab overhaul.
2024-11-24 20:42:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c75145de49 Reminder Record Tab overhaul. 2024-11-24 20:19:48 -07:00
Hargata Softworks
5e3480f9f3 Merge pull request #736 from hargata/Hargata/context.menu.remaster
Remaster the context menu
2024-11-24 14:41:35 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f74fdc8ed4 context menu remaster for both notes and reminders. 2024-11-24 14:40:34 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9ba62f8bf8 prevent multiple gas record edits if selected ids is less than 2 2024-11-24 13:48:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f2b9dea2b6 Added context menu to plan records. 2024-11-24 13:47:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f92aa22375 Remaster the context menu 2024-11-23 22:22:16 -07:00
Hargata Softworks
d65001f412 Merge pull request #735 from hargata/Hargata/costtable.row.highlight
added row highlighting when viewing cost tables.
2024-11-23 15:34:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9ea223cd0a added row highlighting when viewing cost tables. 2024-11-23 15:34:07 -07:00
Hargata Softworks
addf73bc27 Merge pull request #734 from hargata/Hargata/fix.aggregate.label
Fuel Consumption label now updates based on fuel consumption unit sel…
2024-11-23 11:32:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3f853d0d5e Fuel Consumption label now updates based on fuel consumption unit selected. 2024-11-23 11:31:48 -07:00
Hargata Softworks
4c46278be1 Merge pull request #733 from hargata/Hargata/select.mode
Added select mode.
2024-11-22 14:06:53 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0edc51083c Added select mode. 2024-11-22 14:04:36 -07:00
Hargata Softworks
d906bec333 Merge pull request #732 from hargata/Hargata/fix.demo
more minor changes.
2024-11-22 10:57:17 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a47c4942b7 more minor changes. 2024-11-22 10:56:53 -07:00
Hargata Softworks
cd8df9e938 Merge pull request #731 from hargata/Hargata/fix.demo
Update the demo db.
2024-11-22 10:53:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5c37f98274 Update the demo db. 2024-11-22 10:51:08 -07:00
Hargata Softworks
f452436dbd Merge pull request #730 from hargata/Hargata/523
Sort notes in ascending order by description
2024-11-22 10:27:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9375bb103b spacing. 2024-11-22 10:25:40 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3222579cb4 allow notes to be sorted in ascending order as well as minor code clean up. 2024-11-22 10:20:53 -07:00
Hargata Softworks
4348b50e54 Merge pull request #728 from hargata/Hargata/716
additional enhancements
2024-11-21 23:15:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3b7c58b6e8 additional enhancements 2024-11-21 23:15:05 -07:00
Hargata Softworks
664b89a5df Merge pull request #727 from hargata/Hargata/716
Added functionality to display multi year trends.
2024-11-21 22:14:45 -07:00
DESKTOP-T0O5CDB\DESK-555BD
628c22ad61 Added functionality to display multi year trends. 2024-11-21 22:13:08 -07:00
Hargata Softworks
db62a041c4 Merge pull request #726 from hargata/Hargata/454.2
Fixed wording.
2024-11-21 15:07:46 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ea3ffea797 Fixed wording. 2024-11-21 15:07:24 -07:00
Hargata Softworks
c49c8d67a2 Merge pull request #725 from hargata/Hargata/454
Allow users to add supplies onto existing records.
2024-11-21 13:56:24 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3b01df4c56 Updated verbiage 2024-11-21 13:51:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9221516bad fix bug when there are multiple supplies with the same id. 2024-11-20 21:18:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1988cf54f5 Allow users to add supplies onto existing records. 2024-11-20 20:48:38 -07:00
Hargata Softworks
8a8d8979a7 Merge pull request #724 from hargata/Hargata/429
Replenish Supplies By Deleting Records
2024-11-20 12:14:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e983b39f8e Fixed minor bug with plan type not displaying correctly in kiosk mode. 2024-11-20 12:13:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b6c5cd4b46 important bug fix. 2024-11-20 11:57:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
210b27ab25 Allow users to to restore supplies by either deleting the record or deleting the requistion history. 2024-11-20 11:54:29 -07:00
Hargata Softworks
63e9e5ecec Merge pull request #720 from hargata/Hargata/planner.drag.drop.fix
1.4.1 Changes
2024-11-19 14:45:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
771ca15ea2 bump version. 2024-11-19 13:42:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b6b403b5e2 styling fixes. 2024-11-19 13:41:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9976c94481 Merge branch 'main' into Hargata/planner.drag.drop.fix 2024-11-19 13:31:29 -07:00
Hargata Softworks
b9dca8ceae Merge pull request #722 from hargata/Hargata/operationresponse.cleanup.2
Added Conditional OperationalResponse constructor.
2024-11-19 13:14:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
03b9331a9a Added Conditional OperationalResponse constructor. 2024-11-19 13:04:40 -07:00
Hargata Softworks
f629834d6a Merge pull request #721 from hargata/Hargata/operationresponse.cleanup.1
Clean up OperationResponse method Part 1
2024-11-19 12:37:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
25952cce50 Clean up OperationResponse method Part 1 2024-11-19 12:26:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6eba83ccec added enhancement to bulk gas edit modal 2024-11-19 10:46:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c1ce80528 Added user setting for fixing gas consumption to two decimal places. 2024-11-19 10:43:34 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b7851e87b2 Save Vehicle Specific Parameters in SessionStorage. 2024-11-19 09:11:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d8e06fd968 Fix an issue with the drag and drop functionality on the planner page. 2024-11-17 11:02:11 -07:00
Hargata Softworks
c5a5de50a7 Merge pull request #712 from hargata/Hargata/702
simplify code.
2024-11-12 16:02:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7715e7b4ab simplify code. 2024-11-12 16:02:18 -07:00
Hargata Softworks
5a3ed3d3d4 Merge pull request #711 from hargata/Hargata/702
set width
2024-11-12 14:14:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c1e2394194 set width 2024-11-12 13:01:10 -07:00
Hargata Softworks
6abb569df1 Merge pull request #710 from hargata/Hargata/702
User customizable fields in Vehicle Maintenance Report
2024-11-12 12:59:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e59e33bc3b fix bug. 2024-11-12 12:58:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a671dc1937 allow users to define what field sthey want to see in vehicle maintenance report. 2024-11-12 12:54:39 -07:00
Hargata Softworks
426b52aee4 Merge pull request #709 from hargata/Hargata/702
show custom logos in reports and add a banner for credit instead.
2024-11-11 20:36:36 -07:00
DESKTOP-T0O5CDB\DESK-555BD
87fe011565 show custom logos in reports and add a banner for credit instead. 2024-11-11 20:35:09 -07:00
Hargata Softworks
30c03af5b0 Merge pull request #707 from hargata/Hargata/706
Further translation editor enhancements
2024-11-11 11:54:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c9c8fc8705 ensures that all future translations check if the default translation has the key. 2024-11-11 09:06:19 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9cd8030808 see requirements for 706 2024-11-11 09:03:22 -07:00
Hargata Softworks
5411d4152c Merge pull request #704 from Scorpoon/Code-CleanUp-1
#1 Code clean up
2024-11-11 08:08:50 -07:00
Hargata Softworks
c5a05afe1c Merge pull request #703 from Scorpoon/Missing-Translations
Added missing translations
2024-11-10 07:34:50 -07:00
Scorpoon
5b44f2ae33 Renamend UserAccessDataAcces.cs to UserAccessDataAccess.cs 2024-11-10 15:09:19 +01:00
Scorpoon
039b30da4f Optimized imports & removed redundant qualifiers 2024-11-10 14:57:50 +01:00
Scorpoon
df45378bce Added missing translation logic for "Upload documents(optional)" in _UpgradeRecordModal.cshtml 2024-11-10 14:10:49 +01:00
Scorpoon
3729d2c23f - Added translation for "No Recurring Reminders Found" in _RecurringReminderSelector.cshtml
- Added translation for "No Vehicles Available" in _VehicleSelector.cshtml
- Added translations for the DB-Migration tool
- Added the new translation records to the default language file en_US.json
2024-11-10 13:30:07 +01:00
Hargata Softworks
89ece413c8 Merge pull request #696 from hargata/Hargata/custom.widgets
require environment variable to be injected for security reasons.
2024-11-04 11:58:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c1b361397f require environment variable to be injected for security reasons. 2024-11-04 11:57:27 -07:00
Hargata Softworks
ada0715343 Merge pull request #695 from hargata/Hargata/custom.widgets
Custom Widgets
2024-11-04 09:52:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5cdc687f6d backup widgets and further enhance the cleanup api method. 2024-11-04 09:51:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
48f0e16fde additional enhancements to widget editor. 2024-11-04 08:37:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d6b6600ce9 Custom Widgets 2024-11-03 13:22:48 -07:00
Hargata Softworks
82634e9e1b Merge pull request #694 from hargata/Hargata/kiosk.token
more kiosk fixes.
2024-11-03 09:17:29 -07:00
DESKTOP-T0O5CDB\DESK-555BD
225e5e58bc more kiosk fixes. 2024-11-03 09:16:46 -07:00
Hargata Softworks
21d135a8ab Merge pull request #693 from hargata/Hargata/order.missing.supplies
Add functionality to order insufficient supplies for plan templates.
2024-11-02 20:41:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7a0636a48a add cancel button to modal. 2024-11-02 20:40:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0b9ca77281 Add functionality to order insufficient supplies for plan templates. 2024-11-02 19:13:02 -06:00
Hargata Softworks
feaf631b47 Merge pull request #692 from hargata/Hargata/611
Styling fixes for supply selector
2024-11-02 16:45:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec24c19821 Styling fixes for supply selector 2024-11-02 16:44:27 -06:00
Hargata Softworks
36a120fa0f Merge pull request #691 from hargata/Hargata/611
automatic decimal formatting.
2024-11-02 15:47:28 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fb169d5054 automatic decimal formatting. 2024-11-02 15:46:21 -06:00
Hargata Softworks
59fa7b6a1d Merge pull request #688 from hargata/Hargata/526
Add functionality to duplicate records to other vehicles.
2024-11-01 21:08:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
201d832b92 Add functionality to duplicate records to other vehicles. 2024-11-01 21:05:46 -06:00
Hargata Softworks
31b4ef5458 Merge pull request #685 from hargata/Hargata/bump.version
bump version.
2024-11-01 16:59:55 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b06ad0cdaa bump version and kiosk enhancement. 2024-11-01 16:59:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3914bc3b7 bump version. 2024-10-31 23:54:09 -06:00
Hargata Softworks
aea870974b Merge pull request #683 from hargata/Hargata/kiosk
Added kiosk view.
2024-10-31 23:51:21 -06:00
Hargata Softworks
7f6265ba05 Merge pull request #681 from hargata/Hargata/fix.translation.editor
Fixed minor bug with translation editor.
2024-10-31 23:51:00 -06:00
DESKTOP-T0O5CDB\DESK-555BD
85542cb278 minor feature enhancement for #684 2024-10-31 23:49:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f6e0873d83 fix small screen spacing. 2024-10-31 23:41:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
745b262dfc Added kiosk view. 2024-10-31 22:39:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
73df755f70 remove unncessary slash 2024-10-31 18:19:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
762a1b98d3 Fixed minor bug. 2024-10-31 14:49:51 -06:00
Hargata Softworks
571e87dd14 Merge pull request #673 from hargata/Hargata/update.docs
Update screenshots and docs site.
2024-10-31 13:33:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
22eb38709a added show password button, updated configurator, fixed login page width. 2024-10-31 09:41:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
986a8420f8 Merge branch 'main' into Hargata/update.docs 2024-10-30 10:43:33 -06:00
Hargata Softworks
cff5f8e8e9 Merge pull request #679 from hargata/Hargata/518.2
Add ability to re-order tabs.
2024-10-30 09:36:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0df6cea88c backend for saving tab order 2024-10-30 09:33:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
88ff2432f6 Frontend for reordering tabs. 2024-10-29 23:18:31 -06:00
Hargata Softworks
efb7fb999b Merge pull request #677 from hargata/hargata-patch-1
Update screenshots.md
2024-10-28 20:48:35 -06:00
Hargata Softworks
1a2b503791 Update screenshots.md 2024-10-28 20:47:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e68d4ffa3a Merge branch 'main' into Hargata/update.docs 2024-10-28 19:21:50 -06:00
Hargata Softworks
d701a8964f Merge pull request #676 from hargata/Hargata/ios.resizebug
fix bug with iOS closing mobile nav when scrolling
2024-10-28 19:19:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b6929113bc fix bug with iOS closing mobile nav when scrolling 2024-10-28 19:18:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fc1eac1049 Merge branch 'main' into Hargata/update.docs 2024-10-28 15:58:56 -06:00
Hargata Softworks
189659ff1e Merge pull request #674 from hargata/Hargata/fix.wonky.columns
Fix bug where extra fields causes columns to be misaligned.
2024-10-28 15:58:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cb19130119 Fix bug where extra fields causes columns to be misaligned. 2024-10-28 15:52:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1596bf1deb Update screenshots and docs site. 2024-10-28 10:11:34 -06:00
Hargata Softworks
636cbb3d21 Merge pull request #672 from hargata/Hargata/translation.getter
further hardening.
2024-10-28 09:55:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cff7845dd4 further hardening. 2024-10-28 09:19:09 -06:00
Hargata Softworks
2d9cc3507f Merge pull request #671 from hargata/Hargata/translation.getter
Add functionality to download all translations.
2024-10-27 19:04:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4c6ac96dd1 Add functionality to download all translations. 2024-10-27 19:01:53 -06:00
Hargata Softworks
e5a594367e Merge pull request #670 from hargata/Hargata/translation.getter
Add translation getter
2024-10-27 09:54:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
12aaf8c209 spacing issue 2024-10-27 09:53:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cafbb156af Add translation getter 2024-10-27 09:52:21 -06:00
Hargata Softworks
36ac61d848 Merge pull request #669 from hargata/Hargata/translation.editor
fixed height for built in translator.
2024-10-25 16:56:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
df066a593e fixed height for built in translator. 2024-10-25 16:56:24 -06:00
Hargata Softworks
7ac9185e4c Merge pull request #668 from hargata/Hargata/translation.editor
Added translation editor.
2024-10-25 11:08:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
090cbba1a0 Added translation editor. 2024-10-25 11:06:40 -06:00
Hargata Softworks
5e3529f9cc Merge pull request #667 from hargata/Hargata/gps.timeout.fix
Fix distance accuracy
2024-10-24 22:35:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f90ddeabb4 Fix distance accuracy 2024-10-24 22:34:53 -06:00
Hargata Softworks
f54419b993 Merge pull request #666 from hargata/Hargata/gps.timeout.fix
set time out to 4 seconds and max age to 1 second to improve accuracy
2024-10-24 14:14:35 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cbbd7ff25f set time out to 4 seconds and max age to 1 second to improve accuracy 2024-10-24 14:13:47 -06:00
Hargata Softworks
0c2c089828 Merge pull request #664 from hargata/Hargata/485
hide identifier selector if no extra fields configured.
2024-10-23 11:08:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6a1813399f hide identifier selector if no extra fields configured. 2024-10-23 11:07:23 -06:00
Hargata Softworks
4c8d2b3704 Merge pull request #663 from hargata/Hargata/485
Allow users to select any extra field as vehicle identifier.
2024-10-23 10:54:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
23f97ebd46 Allow users to select any extra field as vehicle identifier. 2024-10-23 10:52:41 -06:00
Hargata Softworks
e3dccf0182 Merge pull request #662 from hargata/Hargata/gps.integration
further hardening
2024-10-22 11:32:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
272596878c further hardening 2024-10-22 11:25:48 -06:00
Hargata Softworks
46953e9063 Merge pull request #659 from hargata/Hargata/gps.integration
further bug fixes and hardening.
2024-10-21 10:53:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
30f35855f2 further bug fixes and hardening. 2024-10-21 10:53:09 -06:00
Hargata Softworks
df94132917 Merge pull request #658 from hargata/Hargata/gps.integration
Hargata/gps.integration
2024-10-20 20:59:03 -06:00
DESKTOP-T0O5CDB\DESK-555BD
de37d689be moved variable to partial view. 2024-10-20 20:51:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4f941aad52 add functionality to show sub odometer readings. 2024-10-20 20:41:43 -06:00
Hargata Softworks
2febdf1259 Merge pull request #657 from hargata/Hargata/gps.integration
fixes for GPS integration and experimental feature banner.
2024-10-20 19:12:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
349808a609 fixes for GPS integration and experimental feature banner. 2024-10-20 19:10:23 -06:00
Hargata Softworks
7d58d1bc40 Merge pull request #656 from hargata/Hargata/gps.integration
GPS methods to track vehicle odometer. experimental feature.
2024-10-20 15:31:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5e91a26230 GPS methods to track vehicle odometer. experimental feature. 2024-10-20 15:30:22 -06:00
Hargata Softworks
0942ae1742 Merge pull request #655 from hargata/Hargata/chore.update.deps
CHORE: Update Dependencies
2024-10-18 09:42:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cce9bd29e5 z 2024-10-18 09:42:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
05fdeef862 CHORE: Update Dependencies 2024-10-18 09:41:44 -06:00
Hargata Softworks
dfa08b287c Merge pull request #654 from hargata/Hargata/fix.tabs
improve tabs appearance
2024-10-17 15:06:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
77920f94cd improve tabs appearance 2024-10-17 15:05:20 -06:00
Hargata Softworks
3631a036c6 Merge pull request #653 from hargata/Hargata/616
thumbnail resize using Hermite interpolation
2024-10-17 10:37:54 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1445959d05 thumbnail resize using Hermite interpolation 2024-10-17 10:34:00 -06:00
Hargata Softworks
0b45de5ba8 Merge pull request #652 from hargata/Hargata/fix.titles
Fixed the Titles.
2024-10-16 15:25:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d86cdfcefd Fixed the Titles. 2024-10-16 14:25:19 -06:00
Hargata Softworks
0c7aa304f7 Create CONTRIBUTING.md 2024-10-16 12:36:38 -06:00
Hargata Softworks
e5114f491d Merge pull request #651 from hargata/Hargata/fix.adjustedodomete
1.3.9 Changes
2024-10-15 12:43:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e238bd8a12 translation keys 2024-10-15 11:50:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f53ad74c72 fix bugs with alternate fuel units when the average min and max labels don't exist. 2024-10-14 21:37:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3d456d1cd5 quick styling fix 2024-10-14 10:16:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
de32fe4d85 load sponsors in ajax to account for users with slower internet connection. 2024-10-14 09:42:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
29a90fe814 add note records to attachment export. 2024-10-13 12:25:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3f31f091dc fixed bug with UK units. 2024-10-10 15:41:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
571b558181 allow extra fields edit for multiple odometer records. 2024-10-10 09:38:04 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ed0775ab71 Display fuel mileage unit and fix bug with metric fuel mileages 2024-10-10 10:08:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
19e19d7c15 extra field edit for multiple gas records. 2024-10-09 22:23:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b7e574889c Allow Extra Fields to be edited for multiple records for Service, Repair, and Upgrade Records. 2024-10-09 21:29:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2a66c97254 separated out the import and export methods. 2024-10-09 16:02:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e14ef961b9 improved maintainability by splitting massive controller down to sections. 2024-10-09 15:58:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec2c17be23 updated configurator and streamlined getAndValidateExtraFields method. 2024-10-09 09:13:07 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b5162d2044 configurator webpage 2024-10-05 20:04:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ae428b94d1 Update ENV file to direct users to configurator 2024-10-05 20:04:07 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3350f3c39a confirm deletion. 2024-10-05 18:40:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
68caf82167 moved token into modal. 2024-10-05 18:02:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8c5f1c3a01 fixed minor bug issue in admin panel page and allow for multiple email addresses to generate token for 2024-10-05 17:28:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e1bbc232e1 removed icon. 2024-10-05 16:12:48 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fbc7a39996 Merge branch 'Hargata/admin.panel.enhancement' into Hargata/fix.adjustedodomete 2024-10-05 16:03:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1ce77ad1ee create app schema if not exists without relying on init.sql 2024-10-05 12:03:46 -06:00
DESKTOP-T0O5CDB\DESK-555BD
08cee8029f more ctrl enter forms. 2024-09-29 19:44:28 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e33bcf5e57 re-align some settings switches 2024-09-29 15:25:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cf5ceaa959 Ctrl and Enter to submit record forms 2024-09-29 15:13:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8293bed89c added warning for when user is running LubeLogger without a preconfigured culture. 2024-09-29 10:58:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a830580c27 moved delete vehicle button to edit vehicle modal. 2024-09-29 10:31:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
94c6cca25c add enter event on sweetalert modals see #639 2024-09-28 12:44:09 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78e0a0146c chore: fix deprecated click function on DOM elements 2024-09-28 12:05:48 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ea667ed950 visual enhancements for admin panel 2024-09-28 10:44:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f8b23b5f20 Fix the adjustedodometer endpoint for decimal odometer multiplier 2024-09-25 21:02:49 -06:00
Hargata Softworks
b69749b073 Merge pull request #635 from hargata/Hargata/update.demo
Update demo defaults.
2024-09-24 22:35:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
951eea844f Updated demo default 2024-09-24 22:35:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3e5aff0c9 Updated demo file 2024-09-24 22:33:00 -06:00
Hargata Softworks
ef3e450e4f Merge pull request #634 from hargata/Hargata/api.enhancement.dos
Added documentation and new API endpoint to calculate adjusted odometer.
2024-09-24 22:05:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8272fa20da Added documentation and new API endpoint to calculate adjusted odometer. 2024-09-24 22:04:31 -06:00
Hargata Softworks
9ef0a748a3 Merge pull request #633 from hargata/Hargata/563.619
added flag for optional odometer.
2024-09-24 19:20:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6de928aef4 added flag for optional odometer. 2024-09-24 19:18:13 -06:00
Hargata Softworks
34ff874949 Merge pull request #632 from hargata/Hargata/563.619
make odometer optional
2024-09-24 18:43:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a653f3ac7f make odometer optional 2024-09-24 18:36:59 -06:00
Hargata Softworks
dc96084406 Merge pull request #631 from hargata/Hargata/630
Fix light mode bug.
2024-09-24 18:30:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e056aaa7dc Fix light mode bug. 2024-09-24 18:11:50 -06:00
Hargata Softworks
73bb3c11b4 Merge pull request #628 from hargata/Hargata/adaptive.color.mode
fix label.
2024-09-23 08:37:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c88a040728 fix label. 2024-09-23 08:36:58 -06:00
Hargata Softworks
dfa56c2b12 Merge pull request #627 from hargata/Hargata/adaptive.color.mode
Added switch for adaptive color mode.
2024-09-23 08:34:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
104d4bab52 Added switch for adaptive color mode. 2024-09-23 08:31:54 -06:00
Hargata Softworks
6916161e87 Merge pull request #626 from hargata/Hargata/info.endpoint.update
#482 - Allow Root Users to Login via OIDC
2024-09-22 14:36:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
512852d217 added flag to enable root user to login via OIDC 2024-09-22 14:32:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a61c699417 Added PlanRecords to info endpoint 2024-09-22 13:48:01 -06:00
Hargata Softworks
1e832f014f Merge pull request #625 from hargata/Hargata/update.links
Updated documentation links and removed static documentation.
2024-09-22 10:42:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e9a463e2e5 Updated documentation links and removed static documentation. 2024-09-22 10:41:36 -06:00
Hargata Softworks
d0c19d0436 Merge pull request #624 from hargata/Hargata/618
#618
2024-09-22 10:24:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3428abf358 code clean up for default reminder email and added flag to disable registration on front end. 2024-09-22 10:20:25 -06:00
Hargata Softworks
e6857331e2 Merge pull request #617 from hargata/Hargata/603
adaptive color mode if dark mode is not enabled.
2024-09-13 14:24:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1fd93d2efe update variable name 2024-09-13 14:24:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84e44acbfe adaptive color mode if dark mode is not enabled. 2024-09-13 07:51:32 -06:00
Hargata Softworks
77014c71f2 Merge pull request #614 from hargata/Hargata/517
Add Default Reminder Email Recipient
2024-09-12 11:15:55 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2db01aa4ad reworded response message 2024-09-12 10:00:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6c3fa21cc5 Added environment variable to configure default reminder email recipient. Fixed bug with due date in email body and improved logging and error catching when sending emails. 2024-09-12 09:43:52 -06:00
Hargata Softworks
26b7d101ab Merge pull request #610 from hargata/Hargata/609
Added custom thresholds for reminder.
2024-09-07 13:37:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3c3cdf0cb Added custom thresholds for reminder. 2024-09-07 08:39:26 -06:00
Hargata Softworks
e62fa31485 Merge pull request #608 from hargata/Hargata/606
See #606
2024-09-05 13:19:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5f283c1558 uh 2024-09-05 13:11:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
52163098d2 See #606 2024-09-05 13:04:16 -06:00
Hargata Softworks
dfe27f0577 Merge pull request #605 from hargata/Hargata/598
Added colored icons for Planner items.
2024-09-04 09:23:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e415a3468f Added colored icons for Planner items. 2024-09-03 20:13:07 -06:00
Hargata Softworks
71302a52e4 Merge pull request #602 from hargata/Hargata/light.mode.menu.fix
Fix menu color when in light mode.
2024-09-02 11:41:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
762730eb0f Fix menu color when in light mode. 2024-09-02 11:40:11 -06:00
Hargata Softworks
3999a8a54d Merge pull request #601 from hargata/Hargata/div.zero.hotfix
Fix divide by zero error in Cost Per Mile metric
2024-09-01 11:02:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
065291e146 Fix divide by zero error in Cost Per Mile metric 2024-09-01 11:02:04 -06:00
Hargata Softworks
99376aba1c Merge pull request #600 from hargata/Hargata/537
Add user-configurable dashboard metric
2024-09-01 10:48:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
698e8a0a95 fix default placeholder label for data table. 2024-08-31 23:50:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b1b3a127dc fix bug with data table. 2024-08-31 20:28:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec55c95c71 Allow users to set which dashboard metric they want to see in the garage. 2024-08-31 20:28:32 -06:00
Hargata Softworks
e6eb2b473c Merge pull request #599 from hargata/Hargata/551
Added data table
2024-08-31 17:36:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a4b39820e4 updated label. 2024-08-31 10:39:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4a20c81047 convert to number of days and replace zero values with dashes 2024-08-31 10:33:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b72ab461e5 revert and enhance. 2024-08-31 09:46:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0be498d9bd Added data table 2024-08-30 15:29:09 -06:00
Hargata Softworks
00bad986e5 Merge pull request #596 from hargata/Hargata/reminder.tooltip.fix
Stats API Endpoint
2024-08-29 08:19:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4710e84dcf more stats. 2024-08-27 12:20:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ee55f8c884 added vehicle info API endpoint for dashboards and whatnot. 2024-08-27 12:04:57 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a74116e70 Do not load tooltips on touch screen only device. 2024-08-27 11:07:59 -06:00
210 changed files with 11129 additions and 4367 deletions

6
.env
View File

@@ -7,6 +7,6 @@ MailConfig__Username=""
MailConfig__Password=""
LOGGING__LOGLEVEL__DEFAULT=Error
# * Uncoment this line if you use postgresSQL as database backend.
# * Check the docker-compose.postgresql.yml file
#POSTGRES_CONNECTION="Host=postgres;Username=lubelogger;Password=lubepass;Database=lubelogger;"
# This file is provided as a GUIDELINE ONLY
# Use the LubeLogger Configurator to configure your environment variables
# https://lubelogger.com/configure

48
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,48 @@
# Contributing Guidelines
## Asking Questions
Before submitting a question, please check the [Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting) and search through existing issues.
Ideally, the Issues tab should only consist of bug reports and feature requests instead of debugging your LubeLogger installation.
## Feature Requests
Feature Requests are cool, but we do want to avoid bloat and scope/feature creep.
LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker.
It is not and should not be used for the following:
- Project Management Software(e.g.: Jira)
- Budget Management(e.g: YNAB/Actual)
- Inventory Management
Before submitting a feature request, please consider the following:
- Will this feature benefit more than just a niche subset of users
- Will this feature result in scope creep
- Has this feature already been requested/exist
## Bug Reports
Please fill out the issue template for bug reports.
## Security Vulnerabilities
Contact us via [Email](mailto:hargatasoftworks@gmail.com)
## Submitting Pull Requests
Pull Requests are welcome, but please consider the following:
CI/CD, Bugs, Tech-Debt Related PRs:
- Make sure changes are not breaking.
- If any dependencies are added, they must be cross-platform compatible.
- Please test changes thoroughly.
Feature Related PRs:
- Have you requested this feature in the Issues tab
- Will this feature benefit more than just a niche subset of users
- Will this feature result in scope creep
- Do these changes fix a root cause or is it simply a workaround
Ideally, you should first submit a feature request in the Issues tab before submitting a PR just in case it's something we are already working on.
Note the following:
We(the maintainers) are not responsible for cleaning up, testing, or fixing your changes.
If your changes doesn't have a wide-ranging use-case, has not been thoroughly tested, or cannot be easily maintained, we won't merge your PR.

View File

@@ -10,7 +10,7 @@ assignees: ''
**Checklist**
Please make sure you have performed the following steps before opening a new bug ticket, change `[ ]` to `[x]` to mark it as done
- [ ] I have read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
- [ ] I have read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
- [ ] I have searched through existing issues.
**Description**

10
.gitignore vendored
View File

@@ -1,14 +1,6 @@
.vs/
bin/
obj/
wwwroot/images/
cartracker.db
data/cartracker.db
wwwroot/documents/
wwwroot/temp/
wwwroot/imports/
wwwroot/translations/
config/userConfig.json
data/
CarCareTracker.csproj.user
Properties/launchSettings.json
data/cartracker-log.db

View File

@@ -13,8 +13,8 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="MailKit" Version="4.5.0" />
<PackageReference Include="Npgsql" Version="8.0.3" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Npgsql" Version="8.0.5" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup>

View File

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarCareTracker", "CarCareTracker.csproj", "{0DB85611-6555-4127-A66E-DADD25A16526}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarCareTracker", "CarCareTracker.csproj", "{0DB85611-6555-4127-A66E-DADD25A16526}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

File diff suppressed because it is too large Load Diff

View File

@@ -22,15 +22,46 @@ namespace CarCareTracker.Controllers
{
var viewModel = new AdminViewModel
{
Users = _loginLogic.GetAllUsers(),
Users = _loginLogic.GetAllUsers().OrderBy(x=>x.Id).ToList(),
Tokens = _loginLogic.GetAllTokens()
};
return View(viewModel);
}
public IActionResult GetTokenPartialView()
{
var viewModel = _loginLogic.GetAllTokens();
return PartialView("_Tokens", viewModel);
}
public IActionResult GetUserPartialView()
{
var viewModel = _loginLogic.GetAllUsers().OrderBy(x => x.Id).ToList();
return PartialView("_Users", viewModel);
}
public IActionResult GenerateNewToken(string emailAddress, bool autoNotify)
{
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
return Json(result);
if (emailAddress.Contains(","))
{
string[] emailAddresses = emailAddress.Split(',');
foreach(string emailAdd in emailAddresses)
{
var trimmedEmail = emailAdd.Trim();
if (!string.IsNullOrWhiteSpace(trimmedEmail))
{
var result = _loginLogic.GenerateUserToken(emailAdd.Trim(), autoNotify);
if (!result.Success)
{
//if fail, return prematurely
return Json(result);
}
}
}
var successResponse = OperationResponse.Succeed("Token Generated!");
return Json(successResponse);
} else
{
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
return Json(result);
}
}
[HttpPost]
public IActionResult DeleteToken(int tokenId)

View File

@@ -32,7 +32,7 @@ namespace CarCareTracker.Controllers
var originalFileName = Path.GetFileNameWithoutExtension(file.FileName);
if (originalFileName == "en_US")
{
return Json(new OperationResponse { Success = false, Message = "The translation file name en_US is reserved." });
return Json(OperationResponse.Failed("The translation file name en_US is reserved."));
}
var fileName = UploadFile(file);
//move file from temp to translation folder.
@@ -41,9 +41,9 @@ namespace CarCareTracker.Controllers
if (!string.IsNullOrWhiteSpace(uploadedFilePath))
{
var result = _fileHelper.RenameFile(uploadedFilePath, originalFileName);
return Json(new OperationResponse { Success = result, Message = string.Empty });
return Json(OperationResponse.Conditional(result));
}
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
return Json(OperationResponse.Failed());
}
[HttpPost]
@@ -81,7 +81,7 @@ namespace CarCareTracker.Controllers
private string UploadFile(IFormFile fileToUpload)
{
string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
string fileName = Guid.NewGuid() + Path.GetExtension(fileToUpload.FileName);
@@ -92,5 +92,18 @@ namespace CarCareTracker.Controllers
}
return Path.Combine("/", uploadDirectory, fileName);
}
public IActionResult UploadCoordinates(List<string> coordinates)
{
string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
string fileName = Guid.NewGuid() + ".csv";
string filePath = Path.Combine(uploadPath, fileName);
string fileData = string.Join("\r\n", coordinates);
System.IO.File.WriteAllText(filePath, fileData);
var uploadedFile = new UploadedFiles { Name = "coordinates.csv", Location = Path.Combine("/", uploadDirectory, fileName) };
return Json(uploadedFile);
}
}
}

View File

@@ -22,6 +22,7 @@ namespace CarCareTracker.Controllers
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly ITranslationHelper _translationHelper;
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
@@ -31,7 +32,8 @@ namespace CarCareTracker.Controllers
IFileHelper fileHelper,
IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper)
IReminderHelper reminderHelper,
ITranslationHelper translationHelper)
{
_logger = logger;
_dataAccess = dataAccess;
@@ -43,6 +45,7 @@ namespace CarCareTracker.Controllers
_reminderHelper = reminderHelper;
_loginLogic = loginLogic;
_vehicleLogic = vehicleLogic;
_translationHelper = translationHelper;
}
private int GetUserID()
{
@@ -52,6 +55,59 @@ namespace CarCareTracker.Controllers
{
return View(model: tab);
}
[Route("/kiosk")]
public IActionResult Kiosk(string exclusions, KioskMode kioskMode = KioskMode.Vehicle)
{
try {
var viewModel = new KioskViewModel
{
Exclusions = string.IsNullOrWhiteSpace(exclusions) ? new List<int>() : exclusions.Split(',').Select(x => int.Parse(x)).ToList(),
KioskMode = kioskMode
};
return View(viewModel);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return View(new KioskViewModel());
}
}
[HttpPost]
public IActionResult KioskContent(KioskViewModel kioskParameters)
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
vehiclesStored.RemoveAll(x => kioskParameters.Exclusions.Contains(x.Id));
var userConfig = _config.GetUserConfig(User);
if (userConfig.HideSoldVehicles)
{
vehiclesStored.RemoveAll(x => !string.IsNullOrWhiteSpace(x.SoldDate));
}
switch (kioskParameters.KioskMode)
{
case KioskMode.Vehicle:
{
var kioskResult = _vehicleLogic.GetVehicleInfo(vehiclesStored);
return PartialView("_Kiosk", kioskResult);
}
case KioskMode.Plan:
{
var kioskResult = _vehicleLogic.GetPlans(vehiclesStored, true);
return PartialView("_KioskPlan", kioskResult);
}
break;
case KioskMode.Reminder:
{
var kioskResult = _vehicleLogic.GetReminders(vehiclesStored, false);
return PartialView("_KioskReminder", kioskResult);
}
}
var result = _vehicleLogic.GetVehicleInfo(vehiclesStored);
return PartialView("_Kiosk", result);
}
public IActionResult Garage()
{
var vehiclesStored = _dataAccess.GetVehicles();
@@ -59,22 +115,52 @@ namespace CarCareTracker.Controllers
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
var vehicleViewModels = vehiclesStored.Select(x => new VehicleViewModel
var vehicleViewModels = vehiclesStored.Select(x =>
{
Id = x.Id,
ImageLocation = x.ImageLocation,
Year = x.Year,
Make = x.Make,
Model = x.Model,
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
LastReportedMileage = _vehicleLogic.GetMaxMileage(x.Id),
HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id)
var vehicleVM = new VehicleViewModel
{
Id = x.Id,
ImageLocation = x.ImageLocation,
Year = x.Year,
Make = x.Make,
Model = x.Model,
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
OdometerOptional = x.OdometerOptional,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
DashboardMetrics = x.DashboardMetrics,
VehicleIdentifier = x.VehicleIdentifier
};
//dashboard metrics
if (x.DashboardMetrics.Any())
{
var vehicleRecords = _vehicleLogic.GetVehicleRecords(x.Id);
var userConfig = _config.GetUserConfig(User);
var distanceUnit = x.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km";
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.Default))
{
vehicleVM.LastReportedMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
vehicleVM.HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id, vehicleVM.LastReportedMileage);
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.CostPerMile))
{
var vehicleTotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var totalDistance = maxMileage - minMileage;
vehicleVM.CostPerMile = totalDistance != default ? vehicleTotalCost / totalDistance : 0.00M;
vehicleVM.DistanceUnit = distanceUnit;
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.TotalCost))
{
vehicleVM.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
}
}
return vehicleVM;
}).ToList();
return PartialView("_GarageDisplay", vehicleViewModels);
}
@@ -85,19 +171,7 @@ namespace CarCareTracker.Controllers
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
List<ReminderRecordViewModel> reminders = new List<ReminderRecordViewModel>();
foreach (Vehicle vehicle in vehiclesStored)
{
var vehicleReminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
vehicleReminders.RemoveAll(x => x.Metric == ReminderMetric.Odometer);
//we don't care about mileages so we can basically fake the current vehicle mileage.
if (vehicleReminders.Any())
{
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, 0, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
reminders.AddRange(reminderUrgency);
}
}
var reminders = _vehicleLogic.GetReminders(vehiclesStored, true);
return PartialView("_Calendar", reminders);
}
public IActionResult ViewCalendarReminder(int reminderId)
@@ -115,17 +189,21 @@ namespace CarCareTracker.Controllers
UserConfig = userConfig,
UILanguages = languages
};
return PartialView("_Settings", viewModel);
}
public async Task<IActionResult> Sponsors()
{
try
{
var httpClient = new HttpClient();
var sponsorsData = await httpClient.GetFromJsonAsync<Sponsors>(StaticHelper.SponsorsPath) ?? new Sponsors();
viewModel.Sponsors = sponsorsData;
return PartialView("_Sponsors", sponsorsData);
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve sponsors: {ex.Message}");
return PartialView("_Sponsors", new Sponsors());
}
return PartialView("_Settings", viewModel);
}
[HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig)
@@ -147,10 +225,6 @@ namespace CarCareTracker.Controllers
var result = _config.SaveUserConfig(User, existingConfig);
return Json(result);
}
public IActionResult Privacy()
{
return View();
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult GetExtraFieldsModal(int importMode = 0)
{
@@ -188,7 +262,8 @@ namespace CarCareTracker.Controllers
return Json(result);
}
return Json(false);
} catch (Exception ex)
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(false);
@@ -205,12 +280,12 @@ namespace CarCareTracker.Controllers
var result = _loginLogic.UpdateUserDetails(userId, userAccount);
return Json(result);
}
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage});
return Json(OperationResponse.Failed());
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
return Json(OperationResponse.Failed());
}
}
[HttpGet]
@@ -227,6 +302,260 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name;
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetTranslatorEditor(string userLanguage)
{
var translationData = _translationHelper.GetTranslations(userLanguage);
return PartialView("_TranslationEditor", translationData);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult SaveTranslation(string userLanguage, Dictionary<string, string> translationData)
{
var result = _translationHelper.SaveTranslation(userLanguage, translationData);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult ExportTranslation(Dictionary<string, string> translationData)
{
var result = _translationHelper.ExportTranslation(translationData);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> GetAvailableTranslations()
{
try
{
var httpClient = new HttpClient();
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
return PartialView("_Translations", translations);
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
return PartialView("_Translations", new Translations());
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> DownloadTranslation(string continent, string name)
{
try
{
var httpClient = new HttpClient();
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath(continent, name)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(name, translationData);
if (!result.Success)
{
return Json(false);
}
}
else
{
_logger.LogError($"Unable to download translation: {name}");
return Json(false);
}
return Json(true);
}
catch (Exception ex)
{
_logger.LogError($"Unable to download translation: {ex.Message}");
return Json(false);
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> DownloadAllTranslations()
{
try
{
var httpClient = new HttpClient();
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
int translationsDownloaded = 0;
foreach (string translation in translations.Asia)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Asia", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Africa)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Africa", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Europe)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Europe", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.NorthAmerica)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("NorthAmerica", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.SouthAmerica)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("SouthAmerica", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Oceania)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Oceania", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
if (translationsDownloaded > 0)
{
return Json(OperationResponse.Succeed($"{translationsDownloaded} Translations Downloaded"));
} else
{
return Json(OperationResponse.Failed("No Translations Downloaded"));
}
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
return Json(OperationResponse.Failed());
}
}
public ActionResult GetVehicleSelector(int vehicleId)
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
if (vehicleId != default)
{
vehiclesStored.RemoveAll(x => x.Id == vehicleId);
}
var userConfig = _config.GetUserConfig(User);
if (userConfig.HideSoldVehicles)
{
vehiclesStored.RemoveAll(x => !string.IsNullOrWhiteSpace(x.SoldDate));
}
return PartialView("_VehicleSelector", vehiclesStored);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetCustomWidgetEditor()
{
if (_config.GetCustomWidgetsEnabled())
{
var customWidgetData = _fileHelper.GetWidgets();
return PartialView("_WidgetEditor", customWidgetData);
}
return Json(string.Empty);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult SaveCustomWidgets(string widgetsData)
{
if (_config.GetCustomWidgetsEnabled())
{
var saveResult = _fileHelper.SaveWidgets(widgetsData);
return Json(saveResult);
}
return Json(false);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult DeleteCustomWidgets()
{
if (_config.GetCustomWidgetsEnabled())
{
var deleteResult = _fileHelper.DeleteWidgets();
return Json(deleteResult);
}
return Json(false);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{

View File

@@ -51,6 +51,10 @@ namespace CarCareTracker.Controllers
}
public IActionResult Registration()
{
if (_config.GetServerDisabledRegistration())
{
return RedirectToAction("Index");
}
return View();
}
public IActionResult ForgotPassword()

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Npgsql;
using System.IO.Compression;
using JsonSerializer=System.Text.Json.JsonSerializer;
namespace CarCareTracker.Controllers
{
@@ -14,7 +15,7 @@ namespace CarCareTracker.Controllers
private IConfigHelper _configHelper;
private IFileHelper _fileHelper;
private readonly ILogger<MigrationController> _logger;
public MigrationController(IConfigHelper configHelper, IFileHelper fileHelper, IConfiguration serverConfig, ILogger<MigrationController> logger)
public MigrationController(IConfigHelper configHelper, IFileHelper fileHelper, ILogger<MigrationController> logger)
{
_configHelper = configHelper;
_fileHelper = fileHelper;
@@ -34,6 +35,7 @@ namespace CarCareTracker.Controllers
{
var cmds = new List<string>
{
"CREATE SCHEMA IF NOT EXISTS app",
"CREATE TABLE IF NOT EXISTS app.vehicles (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.collisionrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.upgraderecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
@@ -64,7 +66,7 @@ namespace CarCareTracker.Controllers
{
if (string.IsNullOrWhiteSpace(_configHelper.GetServerPostgresConnection()))
{
return Json(new OperationResponse { Success = false, Message = "Postgres connection not set up" });
return Json(OperationResponse.Failed("Postgres connection not set up"));
}
var tempFolder = $"temp/{Guid.NewGuid()}";
var tempPath = $"{tempFolder}/cartracker.db";
@@ -104,7 +106,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
Vehicle vehicle = System.Text.Json.JsonSerializer.Deserialize<Vehicle>(reader["data"] as string);
Vehicle vehicle = JsonSerializer.Deserialize<Vehicle>(reader["data"] as string);
vehicles.Add(vehicle);
}
}
@@ -122,7 +124,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
repairrecords.Add(System.Text.Json.JsonSerializer.Deserialize<CollisionRecord>(reader["data"] as string));
repairrecords.Add(JsonSerializer.Deserialize<CollisionRecord>(reader["data"] as string));
}
}
foreach (var record in repairrecords)
@@ -139,7 +141,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
upgraderecords.Add(System.Text.Json.JsonSerializer.Deserialize<UpgradeRecord>(reader["data"] as string));
upgraderecords.Add(JsonSerializer.Deserialize<UpgradeRecord>(reader["data"] as string));
}
}
foreach (var record in upgraderecords)
@@ -156,7 +158,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
servicerecords.Add(System.Text.Json.JsonSerializer.Deserialize<ServiceRecord>(reader["data"] as string));
servicerecords.Add(JsonSerializer.Deserialize<ServiceRecord>(reader["data"] as string));
}
}
foreach (var record in servicerecords)
@@ -175,7 +177,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
gasrecords.Add(System.Text.Json.JsonSerializer.Deserialize<GasRecord>(reader["data"] as string));
gasrecords.Add(JsonSerializer.Deserialize<GasRecord>(reader["data"] as string));
}
}
foreach (var record in gasrecords)
@@ -192,7 +194,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
noterecords.Add(System.Text.Json.JsonSerializer.Deserialize<Note>(reader["data"] as string));
noterecords.Add(JsonSerializer.Deserialize<Note>(reader["data"] as string));
}
}
foreach (var record in noterecords)
@@ -209,7 +211,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
odometerrecords.Add(System.Text.Json.JsonSerializer.Deserialize<OdometerRecord>(reader["data"] as string));
odometerrecords.Add(JsonSerializer.Deserialize<OdometerRecord>(reader["data"] as string));
}
}
foreach (var record in odometerrecords)
@@ -226,7 +228,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
reminderrecords.Add(System.Text.Json.JsonSerializer.Deserialize<ReminderRecord>(reader["data"] as string));
reminderrecords.Add(JsonSerializer.Deserialize<ReminderRecord>(reader["data"] as string));
}
}
foreach (var record in reminderrecords)
@@ -245,7 +247,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
planrecords.Add(System.Text.Json.JsonSerializer.Deserialize<PlanRecord>(reader["data"] as string));
planrecords.Add(JsonSerializer.Deserialize<PlanRecord>(reader["data"] as string));
}
}
foreach (var record in planrecords)
@@ -262,7 +264,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
planrecordtemplates.Add(System.Text.Json.JsonSerializer.Deserialize<PlanRecordInput>(reader["data"] as string));
planrecordtemplates.Add(JsonSerializer.Deserialize<PlanRecordInput>(reader["data"] as string));
}
}
foreach (var record in planrecordtemplates)
@@ -279,7 +281,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
supplyrecords.Add(System.Text.Json.JsonSerializer.Deserialize<SupplyRecord>(reader["data"] as string));
supplyrecords.Add(JsonSerializer.Deserialize<SupplyRecord>(reader["data"] as string));
}
}
foreach (var record in supplyrecords)
@@ -296,7 +298,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
taxrecords.Add(System.Text.Json.JsonSerializer.Deserialize<TaxRecord>(reader["data"] as string));
taxrecords.Add(JsonSerializer.Deserialize<TaxRecord>(reader["data"] as string));
}
}
foreach (var record in taxrecords)
@@ -359,7 +361,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
userconfigrecords.Add(System.Text.Json.JsonSerializer.Deserialize<UserConfigData>(reader["data"] as string));
userconfigrecords.Add(JsonSerializer.Deserialize<UserConfigData>(reader["data"] as string));
}
}
foreach (var record in userconfigrecords)
@@ -403,7 +405,7 @@ namespace CarCareTracker.Controllers
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
extrafields.Add(System.Text.Json.JsonSerializer.Deserialize<RecordExtraField>(reader["data"] as string));
extrafields.Add(JsonSerializer.Deserialize<RecordExtraField>(reader["data"] as string));
}
}
foreach (var record in extrafields)
@@ -417,24 +419,24 @@ namespace CarCareTracker.Controllers
#endregion
var destFilePath = $"{fullFolderPath}.zip";
ZipFile.CreateFromDirectory(fullFolderPath, destFilePath);
return Json(new OperationResponse { Success = true, Message = $"/{tempFolder}.zip" });
return Json(OperationResponse.Succeed($"/{tempFolder}.zip"));
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
return Json(OperationResponse.Failed());
}
}
public IActionResult Import(string fileName)
{
if (string.IsNullOrWhiteSpace(_configHelper.GetServerPostgresConnection()))
{
return Json(new OperationResponse { Success = false, Message = "Postgres connection not set up" });
return Json(OperationResponse.Failed("Postgres connection not set up"));
}
var fullFileName = _fileHelper.GetFullFilePath(fileName);
if (string.IsNullOrWhiteSpace(fullFileName))
{
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
return Json(OperationResponse.Failed());
}
try
{
@@ -474,7 +476,7 @@ namespace CarCareTracker.Controllers
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", vehicle.Id);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(vehicle));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(vehicle));
ctext.ExecuteNonQuery();
}
}
@@ -490,7 +492,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -506,7 +508,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -522,7 +524,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -540,7 +542,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -556,7 +558,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -572,7 +574,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -588,7 +590,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -606,7 +608,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -622,7 +624,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -638,7 +640,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -654,7 +656,7 @@ namespace CarCareTracker.Controllers
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("vehicleId", record.VehicleId);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -705,7 +707,7 @@ namespace CarCareTracker.Controllers
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
@@ -737,17 +739,17 @@ namespace CarCareTracker.Controllers
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
#endregion
return Json(new OperationResponse { Success = true, Message = "Data Imported Successfully" });
return Json(OperationResponse.Succeed("Data Imported Successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
return Json(OperationResponse.Failed());
}
}
}

View File

@@ -0,0 +1,209 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
{
var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
//check if the user uses MPG or Liters per 100km.
var userConfig = _config.GetUserConfig(User);
bool useMPG = userConfig.UseMPG;
bool useUKMPG = userConfig.UseUKMPG;
var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG);
if (userConfig.UseDescending)
{
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
}
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
var viewModel = new GasRecordViewModelContainer()
{
UseKwh = vehicleIsElectric,
UseHours = vehicleUseHours,
GasRecords = computedResults
};
return PartialView("_Gas", viewModel);
}
[HttpPost]
public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), gasRecord.VehicleId))
{
return Json(false);
}
if (gasRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Parse(gasRecord.Date),
VehicleId = gasRecord.VehicleId,
Mileage = gasRecord.Mileage,
Notes = $"Auto Insert From Gas Record. {gasRecord.Notes}"
});
}
gasRecord.Files = gasRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord.ToGasRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(gasRecord.ToGasRecord(), gasRecord.Id == default ? "gasrecord.add" : "gasrecord.update", User.Identity.Name));
}
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetAddGasRecordPartialView(int vehicleId)
{
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
return PartialView("_GasModal", new GasRecordInputContainer() { UseKwh = vehicleIsElectric, UseHours = vehicleUseHours, GasRecord = new GasRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields } });
}
[HttpGet]
public IActionResult GetGasRecordForEditById(int gasRecordId)
{
var result = _gasRecordDataAccess.GetGasRecordById(gasRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
var convertedResult = new GasRecordInput
{
Id = result.Id,
Mileage = result.Mileage,
VehicleId = result.VehicleId,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Files = result.Files,
Gallons = result.Gallons,
IsFillToFull = result.IsFillToFull,
MissedFuelUp = result.MissedFuelUp,
Notes = result.Notes,
Tags = result.Tags,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields)
};
var vehicleData = _dataAccess.GetVehicleById(convertedResult.VehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
var viewModel = new GasRecordInputContainer()
{
UseKwh = vehicleIsElectric,
UseHours = vehicleUseHours,
GasRecord = convertedResult
};
return PartialView("_GasModal", viewModel);
}
private bool DeleteGasRecordWithChecks(int gasRecordId)
{
var existingRecord = _gasRecordDataAccess.GetGasRecordById(gasRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
var result = _gasRecordDataAccess.DeleteGasRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteGasRecordById(int gasRecordId)
{
var result = DeleteGasRecordWithChecks(gasRecordId);
return Json(result);
}
[HttpPost]
public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit)
{
var currentConfig = _config.GetUserConfig(User);
currentConfig.PreferredGasUnit = gasUnit;
currentConfig.PreferredGasMileageUnit = fuelMileageUnit;
var result = _config.SaveUserConfig(User, currentConfig);
return Json(result);
}
[HttpPost]
public IActionResult GetGasRecordsEditModal(List<int> recordIds)
{
var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields;
return PartialView("_GasRecordsModal", new GasRecordEditModel { RecordIds = recordIds, EditRecord = new GasRecord { ExtraFields = extraFields } });
}
[HttpPost]
public IActionResult SaveMultipleGasRecords(GasRecordEditModel editModel)
{
var dateIsEdited = editModel.EditRecord.Date != default;
var mileageIsEdited = editModel.EditRecord.Mileage != default;
var consumptionIsEdited = editModel.EditRecord.Gallons != default;
var costIsEdited = editModel.EditRecord.Cost != default;
var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes);
var tagsIsEdited = editModel.EditRecord.Tags.Any();
var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any();
//handle clear overrides
if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---"))
{
editModel.EditRecord.Tags = new List<string>();
}
if (noteIsEdited && editModel.EditRecord.Notes == "---")
{
editModel.EditRecord.Notes = "";
}
bool result = false;
foreach (int recordId in editModel.RecordIds)
{
var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId);
if (dateIsEdited)
{
existingRecord.Date = editModel.EditRecord.Date;
}
if (consumptionIsEdited)
{
existingRecord.Gallons = editModel.EditRecord.Gallons;
}
if (costIsEdited)
{
existingRecord.Cost = editModel.EditRecord.Cost;
}
if (mileageIsEdited)
{
existingRecord.Mileage = editModel.EditRecord.Mileage;
}
if (noteIsEdited)
{
existingRecord.Notes = editModel.EditRecord.Notes;
}
if (tagsIsEdited)
{
existingRecord.Tags = editModel.EditRecord.Tags;
}
if (extraFieldIsEdited)
{
foreach (ExtraField extraField in editModel.EditRecord.ExtraFields)
{
if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name))
{
var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name);
existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name);
existingRecord.ExtraFields.Insert(insertIndex, extraField);
}
else
{
existingRecord.ExtraFields.Add(extraField);
}
}
}
result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord);
}
return Json(result);
}
}
}

View File

@@ -0,0 +1,495 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.MapProfile;
using CarCareTracker.Models;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[HttpGet]
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
{
return PartialView("_BulkDataImporter", mode);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
{
if (vehicleId == default && mode != ImportMode.SupplyRecord)
{
return Json(false);
}
string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
if (mode == ImportMode.ServiceRecord)
{
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
{
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
writer.Dispose();
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.RepairRecord)
{
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
{
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.UpgradeRecord)
{
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
{
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.OdometerRecord)
{
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel
{
Date = x.Date.ToShortDateString(),
Notes = x.Notes,
InitialOdometer = x.InitialMileage.ToString(),
Odometer = x.Mileage.ToString(),
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.SupplyRecord)
{
var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new SupplyRecordExportModel
{
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
PartNumber = x.PartNumber,
PartQuantity = x.Quantity.ToString(),
PartSupplier = x.PartSupplier,
Notes = x.Notes,
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.TaxRecord)
{
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new TaxRecordExportModel
{
Date = x.Date.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Notes = x.Notes,
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteTaxRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.PlanRecord)
{
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any())
{
var exportData = vehicleRecords.Select(x => new PlanRecordExportModel
{
DateCreated = x.DateCreated.ToString("G"),
DateModified = x.DateModified.ToString("G"),
Description = x.Description,
Cost = x.Cost.ToString("C"),
Type = x.ImportMode.ToString(),
Priority = x.Priority.ToString(),
Progress = x.Progress.ToString(),
Notes = x.Notes,
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WritePlanRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
}
else if (mode == ImportMode.GasRecord)
{
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
var exportData = convertedRecords.Select(x => new GasRecordExportModel
{
Date = x.Date.ToString(),
Cost = x.Cost.ToString(),
FuelConsumed = x.Gallons.ToString(),
FuelEconomy = x.MilesPerGallon.ToString(),
Odometer = x.Mileage.ToString(),
IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes,
Tags = string.Join(" ", x.Tags),
ExtraFields = x.ExtraFields
});
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
StaticHelper.WriteGasRecordExportModel(csv, exportData);
}
}
return Json($"/{fileNameToExport}");
}
return Json(false);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
{
if (vehicleId == default && mode != ImportMode.SupplyRecord)
{
return Json(false);
}
if (string.IsNullOrWhiteSpace(fileName))
{
return Json(false);
}
var fullFileName = _fileHelper.GetFullFilePath(fileName);
if (string.IsNullOrWhiteSpace(fullFileName))
{
return Json(false);
}
try
{
using (var reader = new StreamReader(fullFileName))
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture);
config.MissingFieldFound = null;
config.HeaderValidated = null;
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<ImportMapper>();
var records = csv.GetRecords<ImportModel>().ToList();
if (records.Any())
{
var requiredExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)mode).ExtraFields.Where(x => x.IsRequired).Select(y => y.Name);
foreach (ImportModel importModel in records)
{
var parsedDate = DateTime.Now.Date;
if (!string.IsNullOrWhiteSpace(importModel.Date))
{
parsedDate = DateTime.Parse(importModel.Date);
}
else if (!string.IsNullOrWhiteSpace(importModel.Day) && !string.IsNullOrWhiteSpace(importModel.Month) && !string.IsNullOrWhiteSpace(importModel.Year))
{
parsedDate = new DateTime(int.Parse(importModel.Year), int.Parse(importModel.Month), int.Parse(importModel.Day));
}
if (mode == ImportMode.GasRecord)
{
//convert to gas model.
var convertedRecord = new GasRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{
//cost was not given but price is.
//fuelly sometimes exports CSVs without total cost.
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
}
else
{
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
}
if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp))
{
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
convertedRecord.IsFillToFull = !parsedBool;
}
else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
{
var possibleFillToFullValues = new List<string> { "1", "true", "full" };
var parsedBool = possibleFillToFullValues.Contains(importModel.IsFillToFull.Trim().ToLower());
convertedRecord.IsFillToFull = parsedBool;
}
if (!string.IsNullOrWhiteSpace(importModel.MissedFuelUp))
{
var possibleMissedFuelUpValues = new List<string> { "1", "true" };
var parsedBool = possibleMissedFuelUpValues.Contains(importModel.MissedFuelUp.Trim().ToLower());
convertedRecord.MissedFuelUp = parsedBool;
}
//insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error.
if (convertedRecord.Gallons > 0)
{
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = convertedRecord.Date,
VehicleId = convertedRecord.VehicleId,
Mileage = convertedRecord.Mileage,
Notes = $"Auto Insert From Gas Record via CSV Import. {convertedRecord.Notes}"
});
}
}
}
else if (mode == ImportMode.ServiceRecord)
{
var convertedRecord = new ServiceRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {parsedDate.ToShortDateString()}" : 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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = convertedRecord.Date,
VehicleId = convertedRecord.VehicleId,
Mileage = convertedRecord.Mileage,
Notes = $"Auto Insert From Service Record via CSV Import. {convertedRecord.Notes}"
});
}
}
else if (mode == ImportMode.OdometerRecord)
{
var convertedRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
}
else if (mode == ImportMode.PlanRecord)
{
var progressIsEnum = Enum.TryParse(importModel.Progress, out PlanProgress parsedProgress);
var typeIsEnum = Enum.TryParse(importModel.Type, out ImportMode parsedType);
var priorityIsEnum = Enum.TryParse(importModel.Priority, out PlanPriority parsedPriority);
var convertedRecord = new PlanRecord()
{
VehicleId = vehicleId,
DateCreated = DateTime.Parse(importModel.DateCreated),
DateModified = DateTime.Parse(importModel.DateModified),
Progress = parsedProgress,
ImportMode = parsedType,
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),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord);
}
else if (mode == ImportMode.RepairRecord)
{
var convertedRecord = new CollisionRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {parsedDate.ToShortDateString()}" : 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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = convertedRecord.Date,
VehicleId = convertedRecord.VehicleId,
Mileage = convertedRecord.Mileage,
Notes = $"Auto Insert From Repair Record via CSV Import. {convertedRecord.Notes}"
});
}
}
else if (mode == ImportMode.UpgradeRecord)
{
var convertedRecord = new UpgradeRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {parsedDate.ToShortDateString()}" : 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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = convertedRecord.Date,
VehicleId = convertedRecord.VehicleId,
Mileage = convertedRecord.Mileage,
Notes = $"Auto Insert From Upgrade Record via CSV Import. {convertedRecord.Notes}"
});
}
}
else if (mode == ImportMode.SupplyRecord)
{
var convertedRecord = new SupplyRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
PartNumber = importModel.PartNumber,
PartSupplier = importModel.PartSupplier,
Quantity = decimal.Parse(importModel.PartQuantity, NumberStyles.Any),
Description = importModel.Description,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Notes = importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord);
}
else if (mode == ImportMode.TaxRecord)
{
var convertedRecord = new TaxRecord()
{
VehicleId = vehicleId,
Date = parsedDate,
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {parsedDate.ToShortDateString()}" : 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(),
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
}
}
}
}
}
return Json(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Occurred While Bulk Inserting");
return Json(false);
}
}
}
}

View File

@@ -0,0 +1,100 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.OrderByDescending(x => x.Pinned).ThenBy(x => x.Description).ToList();
return PartialView("_Notes", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.Where(x => x.Pinned).ToList();
return Json(result);
}
[HttpPost]
public IActionResult SaveNoteToVehicleId(Note note)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), note.VehicleId))
{
return Json(false);
}
note.Files = note.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
bool isCreate = note.Id == default; //needed here since Notes don't use an input object.
var result = _noteDataAccess.SaveNoteToVehicle(note);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromNoteRecord(note, isCreate ? "noterecord.add" : "noterecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddNotePartialView()
{
return PartialView("_NoteModal", new Note());
}
[HttpGet]
public IActionResult GetNoteForEditById(int noteId)
{
var result = _noteDataAccess.GetNoteById(noteId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
return PartialView("_NoteModal", result);
}
private bool DeleteNoteWithChecks(int noteId)
{
var existingRecord = _noteDataAccess.GetNoteById(noteId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
var result = _noteDataAccess.DeleteNoteById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromNoteRecord(existingRecord, "noterecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteNoteById(int noteId)
{
var result = DeleteNoteWithChecks(noteId);
return Json(result);
}
[HttpPost]
public IActionResult PinNotes(List<int> noteIds, bool isToggle = false, bool pinStatus = false)
{
var result = false;
foreach (int noteId in noteIds)
{
var existingNote = _noteDataAccess.GetNoteById(noteId);
if (isToggle)
{
existingNote.Pinned = !existingNote.Pinned;
}
else
{
existingNote.Pinned = pinStatus;
}
result = _noteDataAccess.SaveNoteToVehicle(existingNote);
}
return Json(result);
}
}
}

View File

@@ -0,0 +1,176 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult ForceRecalculateDistanceByVehicleId(int vehicleId)
{
var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
result = _odometerLogic.AutoConvertOdometerRecord(result);
return Json(result.Any());
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetOdometerRecordsByVehicleId(int vehicleId)
{
var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
//determine if conversion is needed.
if (result.All(x => x.InitialMileage == default))
{
result = _odometerLogic.AutoConvertOdometerRecord(result);
}
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
}
return PartialView("_OdometerRecords", result);
}
[HttpPost]
public IActionResult SaveOdometerRecordToVehicleId(OdometerRecordInput odometerRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), odometerRecord.VehicleId))
{
return Json(false);
}
//move files from temp.
odometerRecord.Files = odometerRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord.ToOdometerRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(odometerRecord.ToOdometerRecord(), odometerRecord.Id == default ? "odometerrecord.add" : "odometerrecord.update", User.Identity.Name));
}
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetAddOdometerRecordPartialView(int vehicleId)
{
return PartialView("_OdometerRecordModal", new OdometerRecordInput() { InitialMileage = _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()), ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields });
}
[HttpPost]
public IActionResult GetOdometerRecordsEditModal(List<int> recordIds)
{
var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields;
return PartialView("_OdometerRecordsModal", new OdometerRecordEditModel { RecordIds = recordIds, EditRecord = new OdometerRecord { ExtraFields = extraFields } });
}
[HttpPost]
public IActionResult SaveMultipleOdometerRecords(OdometerRecordEditModel editModel)
{
var dateIsEdited = editModel.EditRecord.Date != default;
var initialMileageIsEdited = editModel.EditRecord.InitialMileage != default;
var mileageIsEdited = editModel.EditRecord.Mileage != default;
var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes);
var tagsIsEdited = editModel.EditRecord.Tags.Any();
var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any();
//handle clear overrides
if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---"))
{
editModel.EditRecord.Tags = new List<string>();
}
if (noteIsEdited && editModel.EditRecord.Notes == "---")
{
editModel.EditRecord.Notes = "";
}
bool result = false;
foreach (int recordId in editModel.RecordIds)
{
var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId);
if (dateIsEdited)
{
existingRecord.Date = editModel.EditRecord.Date;
}
if (initialMileageIsEdited)
{
existingRecord.InitialMileage = editModel.EditRecord.InitialMileage;
}
if (mileageIsEdited)
{
existingRecord.Mileage = editModel.EditRecord.Mileage;
}
if (noteIsEdited)
{
existingRecord.Notes = editModel.EditRecord.Notes;
}
if (tagsIsEdited)
{
existingRecord.Tags = editModel.EditRecord.Tags;
}
if (extraFieldIsEdited)
{
foreach (ExtraField extraField in editModel.EditRecord.ExtraFields)
{
if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name))
{
var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name);
existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name);
existingRecord.ExtraFields.Insert(insertIndex, extraField);
}
else
{
existingRecord.ExtraFields.Add(extraField);
}
}
}
result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord);
}
return Json(result);
}
[HttpGet]
public IActionResult GetOdometerRecordForEditById(int odometerRecordId)
{
var result = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new OdometerRecordInput
{
Id = result.Id,
Date = result.Date.ToShortDateString(),
InitialMileage = result.InitialMileage,
Mileage = result.Mileage,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields)
};
return PartialView("_OdometerRecordModal", convertedResult);
}
private bool DeleteOdometerRecordWithChecks(int odometerRecordId)
{
var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
var result = _odometerRecordDataAccess.DeleteOdometerRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteOdometerRecordById(int odometerRecordId)
{
var result = DeleteOdometerRecordWithChecks(odometerRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,324 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPlanRecordsByVehicleId(int vehicleId)
{
var result = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
return PartialView("_PlanRecords", result);
}
[HttpPost]
public IActionResult SavePlanRecordToVehicleId(PlanRecordInput planRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), planRecord.VehicleId))
{
return Json(false);
}
//populate createdDate
if (planRecord.Id == default)
{
planRecord.DateCreated = DateTime.Now.ToString("G");
}
planRecord.DateModified = DateTime.Now.ToString("G");
//move files from temp.
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (planRecord.Supplies.Any())
{
planRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description));
if (planRecord.CopySuppliesAttachment)
{
planRecord.Files.AddRange(GetSuppliesAttachments(planRecord.Supplies));
}
}
if (planRecord.DeletedRequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(planRecord.DeletedRequisitionHistory, planRecord.Description);
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord.ToPlanRecord(), planRecord.Id == default ? "planrecord.add" : "planrecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpPost]
public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), planRecord.VehicleId))
{
return Json(OperationResponse.Failed("Access Denied"));
}
//check if template name already taken.
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
if (planRecord.Id == default && existingRecord)
{
return Json(OperationResponse.Failed("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();
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
return Json(OperationResponse.Conditional(result, "Template Added", string.Empty));
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPlanRecordTemplatesForVehicleId(int vehicleId)
{
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicleId);
return PartialView("_PlanRecordTemplateModal", result);
}
[HttpPost]
public IActionResult DeletePlanRecordTemplateById(int planRecordTemplateId)
{
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (existingRecord.Id == default)
{
return Json(false);
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return Json(false);
}
var result = _planRecordTemplateDataAccess.DeletePlanRecordTemplateById(planRecordTemplateId);
return Json(result);
}
[HttpGet]
public IActionResult OrderPlanSupplies(int planRecordTemplateId)
{
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (existingRecord.Id == default)
{
return Json(OperationResponse.Failed("Unable to find template"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return Json(OperationResponse.Failed("Access Denied"));
}
if (existingRecord.Supplies.Any())
{
var suppliesToOrder = CheckSupplyRecordsAvailability(existingRecord.Supplies);
return PartialView("_PlanOrderSupplies", suppliesToOrder);
}
else
{
return Json(OperationResponse.Failed("Template has No Supplies"));
}
}
[HttpPost]
public IActionResult ConvertPlanRecordTemplateToPlanRecord(int planRecordTemplateId)
{
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (existingRecord.Id == default)
{
return Json(OperationResponse.Failed("Unable to find template"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return Json(OperationResponse.Failed("Access Denied"));
}
if (existingRecord.Supplies.Any())
{
//check if all supplies are available
var supplyAvailability = CheckSupplyRecordsAvailability(existingRecord.Supplies);
if (supplyAvailability.Any(x => x.Missing))
{
return Json(OperationResponse.Failed("Missing Supplies, Please Delete This Template and Recreate It."));
}
else if (supplyAvailability.Any(x => x.Insufficient))
{
return Json(OperationResponse.Failed("Insufficient Supplies"));
}
}
if (existingRecord.ReminderRecordId != default)
{
//check if reminder still exists and is still recurring.
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(existingRecord.ReminderRecordId);
if (existingReminder is null || existingReminder.Id == default || !existingReminder.IsRecurring)
{
return Json(OperationResponse.Failed("Missing or Non-recurring Reminder, Please Delete This Template and Recreate It."));
}
}
//populate createdDate
existingRecord.DateCreated = DateTime.Now.ToString("G");
existingRecord.DateModified = DateTime.Now.ToString("G");
existingRecord.Id = default;
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(OperationResponse.Conditional(result, "Plan Record Added", string.Empty));
}
[HttpGet]
public IActionResult GetAddPlanRecordPartialView()
{
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
}
[HttpPost]
public IActionResult GetAddPlanRecordPartialView(PlanRecordInput? planModel)
{
if (planModel is not null)
{
planModel.ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields;
return PartialView("_PlanRecordModal", planModel);
}
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
}
[HttpPost]
public IActionResult UpdatePlanRecordProgress(int planRecordId, PlanProgress planProgress, int odometer = 0)
{
if (planRecordId == default)
{
return Json(false);
}
var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return Json(false);
}
existingRecord.Progress = planProgress;
existingRecord.DateModified = DateTime.Now;
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
if (planProgress == PlanProgress.Done)
{
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Now.Date,
VehicleId = existingRecord.VehicleId,
Mileage = odometer,
Notes = $"Auto Insert From Plan Record: {existingRecord.Description}",
ExtraFields = existingRecord.ExtraFields
});
}
//convert plan record to service/upgrade/repair record.
if (existingRecord.ImportMode == ImportMode.ServiceRecord)
{
var newRecord = new ServiceRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
}
else if (existingRecord.ImportMode == ImportMode.RepairRecord)
{
var newRecord = new CollisionRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
}
else if (existingRecord.ImportMode == ImportMode.UpgradeRecord)
{
var newRecord = new UpgradeRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(newRecord);
}
//push back any reminders
if (existingRecord.ReminderRecordId != default)
{
PushbackRecurringReminderRecordWithChecks(existingRecord.ReminderRecordId, DateTime.Now, odometer);
}
}
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);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new PlanRecordInput
{
Id = result.Id,
Description = result.Description,
DateCreated = result.DateCreated.ToString("G"),
DateModified = result.DateModified.ToString("G"),
ImportMode = result.ImportMode,
Priority = result.Priority,
Progress = result.Progress,
Cost = result.Cost,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
RequisitionHistory = result.RequisitionHistory,
ReminderRecordId = result.ReminderRecordId,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields)
};
return PartialView("_PlanRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeletePlanRecordById(int planRecordId)
{
var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return Json(false);
}
//restore any requisitioned supplies if it has not been converted to other record types.
if (existingRecord.RequisitionHistory.Any() && existingRecord.Progress != PlanProgress.Done)
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete", User.Identity.Name));
}
return Json(result);
}
}
}

View File

@@ -0,0 +1,185 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
{
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
return results;
}
private bool GetAndUpdateVehicleUrgentOrPastDueReminders(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
//check if user wants auto-refresh past-due reminders
if (_config.GetUserConfig(User).EnableAutoReminderRefresh)
{
//check for past due reminders that are eligible for recurring.
var pastDueAndRecurring = result.Where(x => x.Urgency == ReminderUrgency.PastDue && x.IsRecurring);
if (pastDueAndRecurring.Any())
{
foreach (ReminderRecordViewModel reminderRecord in pastDueAndRecurring)
{
//update based on recurring intervals.
//pull reminderRecord based on ID
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecord.Id);
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, null, null);
//save to db.
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
//set urgency to not urgent so it gets excluded in count.
reminderRecord.Urgency = ReminderUrgency.NotUrgent;
}
}
}
//check for very urgent or past due reminders that were not eligible for recurring.
var pastDueAndUrgentReminders = result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
if (pastDueAndUrgentReminders.Any())
{
return true;
}
return false;
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
{
var result = GetAndUpdateVehicleUrgentOrPastDueReminders(vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
result = result.OrderByDescending(x => x.Urgency).ToList();
return PartialView("_ReminderRecords", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId)
{
var result = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
result.RemoveAll(x => !x.IsRecurring);
return PartialView("_RecurringReminderSelector", result);
}
[HttpPost]
public IActionResult PushbackRecurringReminderRecord(int reminderRecordId)
{
var result = PushbackRecurringReminderRecordWithChecks(reminderRecordId, null, null);
return Json(result);
}
private bool PushbackRecurringReminderRecordWithChecks(int reminderRecordId, DateTime? currentDate, int? currentMileage)
{
try
{
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
if (existingReminder is not null && existingReminder.Id != default && existingReminder.IsRecurring)
{
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, currentDate, currentMileage);
//save to db.
var reminderUpdateResult = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
if (!reminderUpdateResult)
{
_logger.LogError("Unable to update reminder either because the reminder no longer exists or is no longer recurring");
return false;
}
return true;
}
else
{
_logger.LogError("Unable to update reminder because it no longer exists.");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
[HttpPost]
public IActionResult SaveReminderRecordToVehicleId(ReminderRecordInput reminderRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), reminderRecord.VehicleId))
{
return Json(false);
}
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord.ToReminderRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord.ToReminderRecord(), reminderRecord.Id == default ? "reminderrecord.add" : "reminderrecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpPost]
public IActionResult GetAddReminderRecordPartialView(ReminderRecordInput? reminderModel)
{
if (reminderModel is not null)
{
return PartialView("_ReminderRecordModal", reminderModel);
}
else
{
return PartialView("_ReminderRecordModal", new ReminderRecordInput());
}
}
[HttpGet]
public IActionResult GetReminderRecordForEditById(int reminderRecordId)
{
var result = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new ReminderRecordInput
{
Id = result.Id,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Notes = result.Notes,
VehicleId = result.VehicleId,
Mileage = result.Mileage,
Metric = result.Metric,
IsRecurring = result.IsRecurring,
UseCustomThresholds = result.UseCustomThresholds,
CustomThresholds = result.CustomThresholds,
ReminderMileageInterval = result.ReminderMileageInterval,
ReminderMonthInterval = result.ReminderMonthInterval,
CustomMileageInterval = result.CustomMileageInterval,
CustomMonthInterval = result.CustomMonthInterval,
Tags = result.Tags
};
return PartialView("_ReminderRecordModal", convertedResult);
}
private bool DeleteReminderRecordWithChecks(int reminderRecordId)
{
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteReminderRecordById(int reminderRecordId)
{
var result = DeleteReminderRecordWithChecks(reminderRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,131 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCollisionRecordsByVehicleId(int vehicleId)
{
var result = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
}
return PartialView("_CollisionRecords", result);
}
[HttpPost]
public IActionResult SaveCollisionRecordToVehicleId(CollisionRecordInput collisionRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), collisionRecord.VehicleId))
{
return Json(false);
}
if (collisionRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Parse(collisionRecord.Date),
VehicleId = collisionRecord.VehicleId,
Mileage = collisionRecord.Mileage,
Notes = $"Auto Insert From Repair Record: {collisionRecord.Description}"
});
}
//move files from temp.
collisionRecord.Files = collisionRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (collisionRecord.Supplies.Any())
{
collisionRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(collisionRecord.Supplies, DateTime.Parse(collisionRecord.Date), collisionRecord.Description));
if (collisionRecord.CopySuppliesAttachment)
{
collisionRecord.Files.AddRange(GetSuppliesAttachments(collisionRecord.Supplies));
}
}
if (collisionRecord.DeletedRequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(collisionRecord.DeletedRequisitionHistory, collisionRecord.Description);
}
//push back any reminders
if (collisionRecord.ReminderRecordId.Any())
{
foreach (int reminderRecordId in collisionRecord.ReminderRecordId)
{
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(collisionRecord.Date), collisionRecord.Mileage);
}
}
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(collisionRecord.ToCollisionRecord(), collisionRecord.Id == default ? "repairrecord.add" : "repairrecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddCollisionRecordPartialView()
{
return PartialView("_CollisionRecordModal", new CollisionRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.RepairRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetCollisionRecordForEditById(int collisionRecordId)
{
var result = _collisionRecordDataAccess.GetCollisionRecordById(collisionRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new CollisionRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Mileage = result.Mileage,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
RequisitionHistory = result.RequisitionHistory,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.RepairRecord).ExtraFields)
};
return PartialView("_CollisionRecordModal", convertedResult);
}
private bool DeleteCollisionRecordWithChecks(int collisionRecordId)
{
var existingRecord = _collisionRecordDataAccess.GetCollisionRecordById(collisionRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
//restore any requisitioned supplies.
if (existingRecord.RequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _collisionRecordDataAccess.DeleteCollisionRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "repairrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteCollisionRecordById(int collisionRecordId)
{
var result = DeleteCollisionRecordWithChecks(collisionRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,657 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetReportPartialView(int vehicleId)
{
//get records
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
var userConfig = _config.GetUserConfig(User);
var viewModel = new ReportViewModel();
//check if custom widgets are configured
viewModel.CustomWidgetsConfigured = _fileHelper.WidgetsExist();
//get totalCostMakeUp
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
{
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
GasRecordSum = gasRecords.Sum(x => x.Cost),
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
TaxRecordSum = taxRecords.Sum(x => x.Cost),
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
};
//get costbymonth
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, 0));
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, 0));
allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, 0));
viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
{
MonthName = x.Key.MonthName,
Cost = x.Sum(y => y.Cost),
DistanceTraveled = x.Max(y => y.DistanceTraveled)
}).ToList();
//get reminders
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now);
viewModel.ReminderMakeUpForVehicle = new ReminderMakeUpForVehicle
{
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
};
//populate year dropdown.
var numbersArray = new List<int>();
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Min(x => x.Date.Year));
}
if (collisionRecords.Any())
{
numbersArray.Add(collisionRecords.Min(x => x.Date.Year));
}
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Min(x => x.Date.Year));
}
if (upgradeRecords.Any())
{
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++)
{
viewModel.Years.Add(DateTime.Now.AddYears(i * -1).Year);
}
//get collaborators
var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId);
viewModel.Collaborators = collaborators;
//get MPG per month.
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default);
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
Cost = x.Average(y => y.MilesPerGallon)
}));
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
}).ToList();
if (invertedFuelMileageUnit)
{
foreach (CostForVehicleByMonth monthMileage in monthlyMileageData)
{
if (monthMileage.Cost != default)
{
monthMileage.Cost = 100 / monthMileage.Cost;
}
}
}
var mpgViewModel = new MPGForVehicleByMonth {
CostData = monthlyMileageData,
Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit,
SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList()
};
viewModel.FuelMileageForVehicleByMonth = mpgViewModel;
return PartialView("_Report", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCollaboratorsForVehicle(int vehicleId)
{
var result = _userLogic.GetCollaboratorsForVehicle(vehicleId);
return PartialView("_Collaborators", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult AddCollaboratorsToVehicle(int vehicleId, string username)
{
var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{
var result = _userLogic.DeleteCollaboratorFromVehicle(userId, vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0)
{
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (year != default)
{
serviceRecords.RemoveAll(x => x.Date.Year != year);
gasRecords.RemoveAll(x => x.Date.Year != year);
collisionRecords.RemoveAll(x => x.Date.Year != year);
taxRecords.RemoveAll(x => x.Date.Year != year);
upgradeRecords.RemoveAll(x => x.Date.Year != year);
}
var viewModel = new CostMakeUpForVehicle
{
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
GasRecordSum = gasRecords.Sum(x => x.Cost),
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
TaxRecordSum = taxRecords.Sum(x => x.Cost),
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
};
return PartialView("_CostMakeUpReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCostTableForVehicle(int vehicleId, int year = 0)
{
var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId);
var serviceRecords = vehicleRecords.ServiceRecords;
var gasRecords = vehicleRecords.GasRecords;
var collisionRecords = vehicleRecords.CollisionRecords;
var taxRecords = vehicleRecords.TaxRecords;
var upgradeRecords = vehicleRecords.UpgradeRecords;
var odometerRecords = vehicleRecords.OdometerRecords;
if (year != default)
{
serviceRecords.RemoveAll(x => x.Date.Year != year);
gasRecords.RemoveAll(x => x.Date.Year != year);
collisionRecords.RemoveAll(x => x.Date.Year != year);
taxRecords.RemoveAll(x => x.Date.Year != year);
upgradeRecords.RemoveAll(x => x.Date.Year != year);
odometerRecords.RemoveAll(x => x.Date.Year != year);
}
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var userConfig = _config.GetUserConfig(User);
var totalDistanceTraveled = maxMileage - minMileage;
var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, serviceRecords, collisionRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords);
var viewModel = new CostTableForVehicle
{
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
GasRecordSum = gasRecords.Sum(x => x.Cost),
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
TaxRecordSum = taxRecords.Sum(x => x.Cost),
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost),
TotalDistance = totalDistanceTraveled,
DistanceUnit = vehicleData.UseHours ? "Cost Per Hour" : userConfig.UseMPG ? "Cost Per Mile" : "Cost Per Kilometer",
NumberOfDays = totalDays
};
return PartialView("_CostTableReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd)
{
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
var viewModel = new ReminderMakeUpForVehicle
{
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
};
return PartialView("_ReminderMakeUpReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetVehicleAttachments(int vehicleId, List<ImportMode> exportTabs)
{
List<GenericReportModel> attachmentData = new List<GenericReportModel>();
if (exportTabs.Contains(ImportMode.ServiceRecord))
{
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.ServiceRecord,
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.RepairRecord))
{
var records = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.RepairRecord,
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.UpgradeRecord))
{
var records = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.UpgradeRecord,
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.GasRecord))
{
var records = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.GasRecord,
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.TaxRecord))
{
var records = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.TaxRecord,
Date = x.Date,
Odometer = 0,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.OdometerRecord))
{
var records = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.OdometerRecord,
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.NoteRecord))
{
var records = _noteDataAccess.GetNotesByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
DataType = ImportMode.NoteRecord,
Date = DateTime.Now,
Odometer = 0,
Files = x.Files
}));
}
if (attachmentData.Any())
{
attachmentData = attachmentData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList();
var result = _fileHelper.MakeAttachmentsExport(attachmentData);
if (string.IsNullOrWhiteSpace(result))
{
return Json(OperationResponse.Failed());
}
return Json(OperationResponse.Succeed(result));
}
else
{
return Json(OperationResponse.Failed("No Attachments Found"));
}
}
public IActionResult GetReportParameters()
{
var viewModel = new ReportParameter() {
VisibleColumns = new List<string> {
nameof(GenericReportModel.DataType),
nameof(GenericReportModel.Date),
nameof(GenericReportModel.Odometer),
nameof(GenericReportModel.Description),
nameof(GenericReportModel.Cost),
nameof(GenericReportModel.Notes)
}
};
//get all extra fields from service records, repairs, upgrades, and tax records.
var recordTypes = new List<int>() { 0, 1, 3, 4 };
var extraFields = new List<string>();
foreach(int recordType in recordTypes)
{
extraFields.AddRange(_extraFieldDataAccess.GetExtraFieldsById(recordType).ExtraFields.Select(x => x.Name));
}
viewModel.ExtraFields = extraFields.Distinct().ToList();
return PartialView("_ReportParameters", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetVehicleHistory(int vehicleId, ReportParameter reportParameter)
{
var vehicleHistory = new VehicleHistoryViewModel();
vehicleHistory.ReportParameters = reportParameter;
vehicleHistory.VehicleData = _dataAccess.GetVehicleById(vehicleId);
var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
var gasViewModels = _gasHelper.GetGasRecordViewModels(vehicleRecords.GasRecords, useMPG, useUKMPG);
//filter by tags
if (reportParameter.Tags.Any())
{
if (reportParameter.TagFilter == TagFilter.Exclude)
{
vehicleRecords.OdometerRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.ServiceRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.CollisionRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.UpgradeRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.TaxRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
gasViewModels.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.GasRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y)));
}
else if (reportParameter.TagFilter == TagFilter.IncludeOnly)
{
vehicleRecords.OdometerRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.ServiceRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.CollisionRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.UpgradeRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.TaxRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
gasViewModels.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
vehicleRecords.GasRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y)));
}
}
//filter by date range.
if (reportParameter.FilterByDateRange && !string.IsNullOrWhiteSpace(reportParameter.StartDate) && !string.IsNullOrWhiteSpace(reportParameter.EndDate))
{
var startDate = DateTime.Parse(reportParameter.StartDate).Date;
var endDate = DateTime.Parse(reportParameter.EndDate).Date;
//validate date range
if (endDate >= startDate) //allow for same day.
{
vehicleHistory.StartDate = reportParameter.StartDate;
vehicleHistory.EndDate = reportParameter.EndDate;
//remove all records with dates after the end date and dates before the start date.
vehicleRecords.OdometerRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
vehicleRecords.ServiceRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
vehicleRecords.CollisionRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
vehicleRecords.UpgradeRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
vehicleRecords.TaxRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
gasViewModels.RemoveAll(x => DateTime.Parse(x.Date).Date > endDate || DateTime.Parse(x.Date).Date < startDate);
vehicleRecords.GasRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate);
}
}
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
vehicleHistory.Odometer = maxMileage.ToString("N0");
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var distanceTraveled = maxMileage - minMileage;
if (!string.IsNullOrWhiteSpace(vehicleHistory.VehicleData.PurchaseDate))
{
var endDate = vehicleHistory.VehicleData.SoldDate;
int daysOwned = 0;
if (string.IsNullOrWhiteSpace(endDate))
{
endDate = DateTime.Now.ToShortDateString();
}
try
{
daysOwned = (DateTime.Parse(endDate) - DateTime.Parse(vehicleHistory.VehicleData.PurchaseDate)).Days;
vehicleHistory.DaysOwned = daysOwned.ToString("N0");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
vehicleHistory.DaysOwned = string.Empty;
}
//calculate depreciation
var totalDepreciation = vehicleHistory.VehicleData.PurchasePrice - vehicleHistory.VehicleData.SoldPrice;
//we only calculate depreciation if a sold price is provided.
if (totalDepreciation != default && vehicleHistory.VehicleData.SoldPrice != default)
{
vehicleHistory.TotalDepreciation = totalDepreciation;
if (daysOwned != default)
{
vehicleHistory.DepreciationPerDay = Math.Abs(totalDepreciation / daysOwned);
}
if (distanceTraveled != default)
{
vehicleHistory.DepreciationPerMile = Math.Abs(totalDepreciation / distanceTraveled);
}
}
}
List<GenericReportModel> reportData = new List<GenericReportModel>();
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
vehicleHistory.DistanceUnit = vehicleHistory.VehicleData.UseHours ? "h" : useMPG ? "mi." : "km";
vehicleHistory.TotalGasCost = gasViewModels.Sum(x => x.Cost);
vehicleHistory.TotalCost = vehicleRecords.ServiceRecords.Sum(x => x.Cost) + vehicleRecords.CollisionRecords.Sum(x => x.Cost) + vehicleRecords.UpgradeRecords.Sum(x => x.Cost) + vehicleRecords.TaxRecords.Sum(x => x.Cost);
if (distanceTraveled != default)
{
vehicleHistory.DistanceTraveled = distanceTraveled.ToString("N0");
vehicleHistory.TotalCostPerMile = vehicleHistory.TotalCost / distanceTraveled;
vehicleHistory.TotalGasCostPerMile = vehicleHistory.TotalGasCost / distanceTraveled;
}
var averageMPG = "0";
if (gasViewModels.Any())
{
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG);
}
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleHistory.VehicleData.IsElectric, vehicleHistory.VehicleData.UseHours, useMPG, useUKMPG);
if (fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l")
{
//conversion needed.
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
if (newAverageMPG != 0)
{
newAverageMPG = 100 / newAverageMPG;
}
averageMPG = newAverageMPG.ToString("F");
fuelEconomyMileageUnit = preferredFuelMileageUnit;
}
vehicleHistory.MPG = $"{averageMPG} {fuelEconomyMileageUnit}";
//insert servicerecords
reportData.AddRange(vehicleRecords.ServiceRecords.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Description = x.Description,
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.ServiceRecord,
ExtraFields = x.ExtraFields
}));
//repair records
reportData.AddRange(vehicleRecords.CollisionRecords.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Description = x.Description,
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.RepairRecord,
ExtraFields = x.ExtraFields
}));
reportData.AddRange(vehicleRecords.UpgradeRecords.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Description = x.Description,
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.UpgradeRecord,
ExtraFields = x.ExtraFields
}));
reportData.AddRange(vehicleRecords.TaxRecords.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = 0,
Description = x.Description,
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.TaxRecord,
ExtraFields = x.ExtraFields
}));
vehicleHistory.VehicleHistory = reportData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList();
return PartialView("_VehicleHistory", vehicleHistory);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetMonthMPGByVehicle(int vehicleId, int year = 0)
{
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var userConfig = _config.GetUserConfig(User);
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
if (year != 0)
{
mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year);
}
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
Cost = x.Average(y => y.MilesPerGallon)
}));
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
}).ToList();
if (invertedFuelMileageUnit)
{
foreach (CostForVehicleByMonth monthMileage in monthlyMileageData)
{
if (monthMileage.Cost != default)
{
monthMileage.Cost = 100 / monthMileage.Cost;
}
}
}
var mpgViewModel = new MPGForVehicleByMonth
{
CostData = monthlyMileageData,
Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit,
SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList()
};
return PartialView("_MPGByMonthReport", mpgViewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
{
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
if (selectedMetrics.Contains(ImportMode.ServiceRecord))
{
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year));
}
if (selectedMetrics.Contains(ImportMode.RepairRecord))
{
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetRepairRecordSum(repairRecords, year));
}
if (selectedMetrics.Contains(ImportMode.UpgradeRecord))
{
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year));
}
if (selectedMetrics.Contains(ImportMode.GasRecord))
{
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year));
}
if (selectedMetrics.Contains(ImportMode.TaxRecord))
{
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year));
}
if (selectedMetrics.Contains(ImportMode.OdometerRecord))
{
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, year));
}
var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
{
MonthName = x.Key.MonthName,
Cost = x.Sum(y => y.Cost),
DistanceTraveled = x.Max(y => y.DistanceTraveled)
}).ToList();
return PartialView("_GasCostByMonthReport", groupedRecord);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetCostByMonthAndYearByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
{
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
if (selectedMetrics.Contains(ImportMode.ServiceRecord))
{
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year, true));
}
if (selectedMetrics.Contains(ImportMode.RepairRecord))
{
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetRepairRecordSum(repairRecords, year, true));
}
if (selectedMetrics.Contains(ImportMode.UpgradeRecord))
{
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year, true));
}
if (selectedMetrics.Contains(ImportMode.GasRecord))
{
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year, true));
}
if (selectedMetrics.Contains(ImportMode.TaxRecord))
{
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year, true));
}
if (selectedMetrics.Contains(ImportMode.OdometerRecord))
{
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, year, true));
}
var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId, x.Year }).OrderByDescending(x=>x.Key.Year).Select(x => new CostForVehicleByMonth
{
Year = x.Key.Year,
MonthName = x.Key.MonthName,
Cost = x.Sum(y => y.Cost),
DistanceTraveled = x.Max(y => y.DistanceTraveled),
MonthId = x.Key.MonthId
}).ToList();
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var userConfig = _config.GetUserConfig(User);
var viewModel = new CostDistanceTableForVehicle { CostData = groupedRecord };
viewModel.DistanceUnit = vehicleData.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km";
return PartialView("_CostDistanceTableReport", viewModel);
}
[HttpGet]
public IActionResult GetAdditionalWidgets()
{
var widgets = _fileHelper.GetWidgets();
return PartialView("_ReportWidgets", widgets);
}
}
}

View File

@@ -0,0 +1,131 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetServiceRecordsByVehicleId(int vehicleId)
{
var result = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
}
return PartialView("_ServiceRecords", result);
}
[HttpPost]
public IActionResult SaveServiceRecordToVehicleId(ServiceRecordInput serviceRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), serviceRecord.VehicleId))
{
return Json(false);
}
if (serviceRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Parse(serviceRecord.Date),
VehicleId = serviceRecord.VehicleId,
Mileage = serviceRecord.Mileage,
Notes = $"Auto Insert From Service Record: {serviceRecord.Description}"
});
}
//move files from temp.
serviceRecord.Files = serviceRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (serviceRecord.Supplies.Any())
{
serviceRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(serviceRecord.Supplies, DateTime.Parse(serviceRecord.Date), serviceRecord.Description));
if (serviceRecord.CopySuppliesAttachment)
{
serviceRecord.Files.AddRange(GetSuppliesAttachments(serviceRecord.Supplies));
}
}
if (serviceRecord.DeletedRequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(serviceRecord.DeletedRequisitionHistory, serviceRecord.Description);
}
//push back any reminders
if (serviceRecord.ReminderRecordId.Any())
{
foreach (int reminderRecordId in serviceRecord.ReminderRecordId)
{
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(serviceRecord.Date), serviceRecord.Mileage);
}
}
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(serviceRecord.ToServiceRecord(), serviceRecord.Id == default ? "servicerecord.add" : "servicerecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddServiceRecordPartialView()
{
return PartialView("_ServiceRecordModal", new ServiceRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetServiceRecordForEditById(int serviceRecordId)
{
var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new ServiceRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Mileage = result.Mileage,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
RequisitionHistory = result.RequisitionHistory,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields)
};
return PartialView("_ServiceRecordModal", convertedResult);
}
private bool DeleteServiceRecordWithChecks(int serviceRecordId)
{
var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
//restore any requisitioned supplies.
if (existingRecord.RequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _serviceRecordDataAccess.DeleteServiceRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "servicerecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteServiceRecordById(int serviceRecordId)
{
var result = DeleteServiceRecordWithChecks(serviceRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,215 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
private List<SupplyAvailability> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
{
//returns empty string if all supplies are available
var result = new List<SupplyAvailability>();
foreach (SupplyUsage supply in supplyUsage)
{
//get supply record.
var supplyData = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
if (supplyData == null)
{
result.Add(new SupplyAvailability { Missing = true });
}
else
{
result.Add(new SupplyAvailability { Missing = false, Description = supplyData.Description, Required = supply.Quantity, InStock = supplyData.Quantity });
}
}
return result;
}
private List<UploadedFiles> GetSuppliesAttachments(List<SupplyUsage> supplyUsage)
{
List<UploadedFiles> results = new List<UploadedFiles>();
foreach (SupplyUsage supply in supplyUsage)
{
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
results.AddRange(result.Files);
}
return results;
}
private List<SupplyUsageHistory> RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage, DateTime dateRequisitioned, string usageDescription)
{
List<SupplyUsageHistory> results = new List<SupplyUsageHistory>();
foreach (SupplyUsage supply in supplyUsage)
{
//get supply record.
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
var unitCost = (result.Quantity != 0) ? result.Cost / result.Quantity : 0;
//deduct quantity used.
result.Quantity -= supply.Quantity;
//deduct cost.
result.Cost -= (supply.Quantity * unitCost);
//check decimal places to ensure that it always has a max of 3 decimal places.
var roundedDecimal = decimal.Round(result.Cost, 3);
if (roundedDecimal != result.Cost)
{
//Too many decimals
result.Cost = roundedDecimal;
}
//create new requisitionrrecord
var requisitionRecord = new SupplyUsageHistory
{
Id = supply.SupplyId,
Date = dateRequisitioned,
Description = usageDescription,
Quantity = supply.Quantity,
Cost = (supply.Quantity * unitCost)
};
result.RequisitionHistory.Add(requisitionRecord);
//save
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(result);
requisitionRecord.Description = result.Description; //change the name of the description for plan/service/repair/upgrade records
requisitionRecord.PartNumber = result.PartNumber; //populate part number if not displayed in supplies modal.
results.Add(requisitionRecord);
}
return results;
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetSupplyRecordsByVehicleId(int vehicleId)
{
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
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)
{
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
if (_config.GetServerEnableShopSupplies())
{
result.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
}
result.RemoveAll(x => x.Quantity <= 0);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
var viewModel = new SupplyUsageViewModel
{
Supplies = result
};
return PartialView("_SupplyUsage", viewModel);
}
[HttpPost]
public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord)
{
//move files from temp.
supplyRecord.Files = supplyRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(supplyRecord.ToSupplyRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromSupplyRecord(supplyRecord.ToSupplyRecord(), supplyRecord.Id == default ? "supplyrecord.add" : "supplyrecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddSupplyRecordPartialView()
{
return PartialView("_SupplyRecordModal", new SupplyRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetSupplyRecordForEditById(int supplyRecordId)
{
var result = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId);
if (result.RequisitionHistory.Any())
{
//requisition history when viewed through the supply is always immutable.
result.RequisitionHistory = result.RequisitionHistory.Select(x => new SupplyUsageHistory { Id = default, Cost = x.Cost, Description = x.Description, Date = x.Date, PartNumber = x.PartNumber, Quantity = x.Quantity }).ToList();
}
//convert to Input object.
var convertedResult = new SupplyRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
PartNumber = result.PartNumber,
Quantity = result.Quantity,
PartSupplier = result.PartSupplier,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
RequisitionHistory = result.RequisitionHistory,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields)
};
return PartialView("_SupplyRecordModal", convertedResult);
}
private bool DeleteSupplyRecordWithChecks(int supplyRecordId)
{
var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId);
if (existingRecord.VehicleId != default)
{
//security check only if not editing shop supply.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
}
var result = _supplyRecordDataAccess.DeleteSupplyRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromSupplyRecord(existingRecord, "supplyrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteSupplyRecordById(int supplyRecordId)
{
var result = DeleteSupplyRecordWithChecks(supplyRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,121 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetTaxRecordsByVehicleId(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
return PartialView("_TaxRecords", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult CheckRecurringTaxRecords(int vehicleId)
{
try
{
var result = _vehicleLogic.UpdateRecurringTaxes(vehicleId);
return Json(result);
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(false);
}
}
[HttpPost]
public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), taxRecord.VehicleId))
{
return Json(false);
}
//move files from temp.
taxRecord.Files = taxRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
//push back any reminders
if (taxRecord.ReminderRecordId.Any())
{
foreach (int reminderRecordId in taxRecord.ReminderRecordId)
{
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(taxRecord.Date), null);
}
}
var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord());
_vehicleLogic.UpdateRecurringTaxes(taxRecord.VehicleId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(taxRecord.ToTaxRecord(), taxRecord.Id == default ? "taxrecord.add" : "taxrecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddTaxRecordPartialView()
{
return PartialView("_TaxRecordModal", new TaxRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetTaxRecordForEditById(int taxRecordId)
{
var result = _taxRecordDataAccess.GetTaxRecordById(taxRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new TaxRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Notes = result.Notes,
VehicleId = result.VehicleId,
IsRecurring = result.IsRecurring,
RecurringInterval = result.RecurringInterval,
CustomMonthInterval = result.CustomMonthInterval,
Files = result.Files,
Tags = result.Tags,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields)
};
return PartialView("_TaxRecordModal", convertedResult);
}
private bool DeleteTaxRecordWithChecks(int taxRecordId)
{
var existingRecord = _taxRecordDataAccess.GetTaxRecordById(taxRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
var result = _taxRecordDataAccess.DeleteTaxRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteTaxRecordById(int taxRecordId)
{
var result = DeleteTaxRecordWithChecks(taxRecordId);
return Json(result);
}
}
}

View File

@@ -0,0 +1,131 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
{
var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
}
return PartialView("_UpgradeRecords", result);
}
[HttpPost]
public IActionResult SaveUpgradeRecordToVehicleId(UpgradeRecordInput upgradeRecord)
{
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), upgradeRecord.VehicleId))
{
return Json(false);
}
if (upgradeRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Parse(upgradeRecord.Date),
VehicleId = upgradeRecord.VehicleId,
Mileage = upgradeRecord.Mileage,
Notes = $"Auto Insert From Upgrade Record: {upgradeRecord.Description}"
});
}
//move files from temp.
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (upgradeRecord.Supplies.Any())
{
upgradeRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Description));
if (upgradeRecord.CopySuppliesAttachment)
{
upgradeRecord.Files.AddRange(GetSuppliesAttachments(upgradeRecord.Supplies));
}
}
if (upgradeRecord.DeletedRequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(upgradeRecord.DeletedRequisitionHistory, upgradeRecord.Description);
}
//push back any reminders
if (upgradeRecord.ReminderRecordId.Any())
{
foreach (int reminderRecordId in upgradeRecord.ReminderRecordId)
{
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Mileage);
}
}
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(upgradeRecord.ToUpgradeRecord(), upgradeRecord.Id == default ? "upgraderecord.add" : "upgraderecord.update", User.Identity.Name));
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddUpgradeRecordPartialView()
{
return PartialView("_UpgradeRecordModal", new UpgradeRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetUpgradeRecordForEditById(int upgradeRecordId)
{
var result = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId))
{
return Redirect("/Error/Unauthorized");
}
//convert to Input object.
var convertedResult = new UpgradeRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Mileage = result.Mileage,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
RequisitionHistory = result.RequisitionHistory,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields)
};
return PartialView("_UpgradeRecordModal", convertedResult);
}
private bool DeleteUpgradeRecordWithChecks(int upgradeRecordId)
{
var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId);
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
return false;
}
//restore any requisitioned supplies.
if (existingRecord.RequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.delete", User.Identity.Name));
}
return result;
}
[HttpPost]
public IActionResult DeleteUpgradeRecordById(int upgradeRecordId)
{
var result = DeleteUpgradeRecordWithChecks(upgradeRecordId);
return Json(result);
}
}
}

File diff suppressed because it is too large Load Diff

9
Enum/DashboardMetric.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public enum DashboardMetric
{
Default = 0,
TotalCost = 1,
CostPerMile = 2
}
}

10
Enum/KioskMode.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public enum KioskMode
{
Vehicle = 0,
Plan = 1,
Reminder = 2,
Cycle = 3
}
}

8
Enum/TagFilter.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public enum TagFilter
{
Exclude = 0,
IncludeOnly = 1
}
}

View File

@@ -1,7 +1,6 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{

View File

@@ -1,7 +1,6 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{

View File

@@ -18,7 +18,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -18,7 +18,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -16,7 +16,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -1,7 +1,6 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Npgsql;
using System.Net.Mail;
namespace CarCareTracker.External.Implementations
{
@@ -17,7 +16,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -16,7 +16,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();

View File

@@ -19,27 +19,34 @@ namespace CarCareTracker.Filter
{
if (!filterContext.HttpContext.User.IsInRole(nameof(UserData.IsRootUser)))
{
var vehicleId = int.Parse(filterContext.ActionArguments["vehicleId"].ToString());
if (vehicleId != default)
if (filterContext.ActionArguments.ContainsKey("vehicleId"))
{
var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
if (!_userLogic.UserCanEditVehicle(userId, vehicleId))
var vehicleId = int.Parse(filterContext.ActionArguments["vehicleId"].ToString());
if (vehicleId != default)
{
filterContext.Result = new RedirectResult("/Error/Unauthorized");
var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
if (!_userLogic.UserCanEditVehicle(userId, vehicleId))
{
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
}
else
{
var shopSupplyEndpoints = new List<string> { "ImportToVehicleIdFromCsv", "GetSupplyRecordsByVehicleId", "ExportFromVehicleToCsv" };
if (shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()) && !_config.GetServerEnableShopSupplies())
{
//user trying to access shop supplies but shop supplies is not enabled by root user.
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
else if (!shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()))
{
//user trying to access any other endpoints using 0 as vehicle id.
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
}
} else
{
var shopSupplyEndpoints = new List<string> { "ImportToVehicleIdFromCsv", "GetSupplyRecordsByVehicleId", "ExportFromVehicleToCsv" };
if (shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()) && !_config.GetServerEnableShopSupplies())
{
//user trying to access shop supplies but shop supplies is not enabled by root user.
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
else if (!shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()))
{
//user trying to access any other endpoints using 0 as vehicle id.
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
}
}

View File

@@ -2,6 +2,7 @@
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Claims;
using System.Text.Json;
namespace CarCareTracker.Helper
{
@@ -9,47 +10,55 @@ namespace CarCareTracker.Helper
{
OpenIDConfig GetOpenIDConfig();
ReminderUrgencyConfig GetReminderUrgencyConfig();
MailConfig GetMailConfig();
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
bool AuthenticateRootUser(string username, string password);
bool AuthenticateRootUserOIDC(string email);
string GetWebHookUrl();
bool GetCustomWidgetsEnabled();
string GetMOTD();
string GetLogoUrl();
string GetServerLanguage();
bool GetServerDisabledRegistration();
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
public bool DeleteUserConfig(int userId);
bool DeleteUserConfig(int userId);
bool GetInvariantApi();
}
public class ConfigHelper : IConfigHelper
{
private readonly IConfiguration _config;
private readonly IUserConfigDataAccess _userConfig;
private readonly ILogger<IConfigHelper> _logger;
private IMemoryCache _cache;
public ConfigHelper(IConfiguration serverConfig,
IUserConfigDataAccess userConfig,
IMemoryCache memoryCache)
IMemoryCache memoryCache,
ILogger<IConfigHelper> logger)
{
_config = serverConfig;
_userConfig = userConfig;
_cache = memoryCache;
_logger = logger;
}
public string GetWebHookUrl()
{
var webhook = _config["LUBELOGGER_WEBHOOK"];
if (string.IsNullOrWhiteSpace(webhook))
{
webhook = "";
}
var webhook = CheckString("LUBELOGGER_WEBHOOK");
return webhook;
}
public bool GetCustomWidgetsEnabled()
{
return CheckBool(CheckString("LUBELOGGER_CUSTOM_WIDGETS"));
}
public bool GetInvariantApi()
{
return CheckBool(CheckString("LUBELOGGER_INVARIANT_API"));
}
public string GetMOTD()
{
var motd = _config["LUBELOGGER_MOTD"];
if (string.IsNullOrWhiteSpace(motd))
{
motd = "";
}
var motd = CheckString("LUBELOGGER_MOTD");
return motd;
}
public OpenIDConfig GetOpenIDConfig()
@@ -62,51 +71,59 @@ namespace CarCareTracker.Helper
ReminderUrgencyConfig reminderUrgencyConfig = _config.GetSection("ReminderUrgencyConfig").Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig();
return reminderUrgencyConfig;
}
public MailConfig GetMailConfig()
{
MailConfig mailConfig = _config.GetSection("MailConfig").Get<MailConfig>() ?? new MailConfig();
return mailConfig;
}
public string GetLogoUrl()
{
var logoUrl = _config["LUBELOGGER_LOGO_URL"];
if (string.IsNullOrWhiteSpace(logoUrl))
{
logoUrl = "/defaults/lubelogger_logo.png";
}
var logoUrl = CheckString("LUBELOGGER_LOGO_URL", "/defaults/lubelogger_logo.png");
return logoUrl;
}
public string GetAllowedFileUploadExtensions()
{
var allowedFileExtensions = _config["LUBELOGGER_ALLOWED_FILE_EXTENSIONS"];
if (string.IsNullOrWhiteSpace(allowedFileExtensions)){
return StaticHelper.DefaultAllowedFileExtensions;
}
var allowedFileExtensions = CheckString("LUBELOGGER_ALLOWED_FILE_EXTENSIONS", StaticHelper.DefaultAllowedFileExtensions);
return allowedFileExtensions;
}
public bool AuthenticateRootUser(string username, string password)
{
var rootUsername = _config[nameof(UserConfig.UserNameHash)] ?? string.Empty;
var rootPassword = _config[nameof(UserConfig.UserPasswordHash)] ?? string.Empty;
var rootUsername = CheckString(nameof(UserConfig.UserNameHash));
var rootPassword = CheckString(nameof(UserConfig.UserPasswordHash));
if (string.IsNullOrWhiteSpace(rootUsername) || string.IsNullOrWhiteSpace(rootPassword))
{
return false;
}
return username == rootUsername && password == rootPassword;
}
public bool AuthenticateRootUserOIDC(string email)
{
var rootEmail = CheckString(nameof(UserConfig.DefaultReminderEmail));
var rootUserOIDC = CheckBool(CheckString(nameof(UserConfig.EnableRootUserOIDC)));
if (!rootUserOIDC || string.IsNullOrWhiteSpace(rootEmail))
{
return false;
}
return email == rootEmail;
}
public string GetServerLanguage()
{
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
var serverLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US");
return serverLanguage;
}
public bool GetServerDisabledRegistration()
{
var registrationDisabled = CheckBool(CheckString(nameof(UserConfig.DisableRegistration)));
return registrationDisabled;
}
public string GetServerPostgresConnection()
{
if (!string.IsNullOrWhiteSpace(_config["POSTGRES_CONNECTION"]))
{
return _config["POSTGRES_CONNECTION"];
} else
{
return string.Empty;
}
var postgresConnection = CheckString("POSTGRES_CONNECTION");
return postgresConnection;
}
public bool GetServerEnableShopSupplies()
{
return bool.Parse(_config[nameof(UserConfig.EnableShopSupplies)] ?? "false");
return CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies)));
}
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
{
@@ -124,18 +141,19 @@ namespace CarCareTracker.Helper
if (!File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
configData.EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)] ?? "false");
configData.UserNameHash = _config[nameof(UserConfig.UserNameHash)] ?? string.Empty;
configData.UserPasswordHash = _config[nameof(UserConfig.UserPasswordHash)] ?? string.Empty;
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(configData));
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(configData));
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex.Message);
return false;
}
} else
@@ -156,31 +174,73 @@ namespace CarCareTracker.Helper
var result = _userConfig.DeleteUserConfig(userId);
return result;
}
private bool CheckBool(string value, bool defaultValue = false)
{
try
{
if (string.IsNullOrWhiteSpace(value))
{
return defaultValue;
}
else if (bool.TryParse(value, out bool result))
{
return result;
}
else
{
return defaultValue;
}
} catch (Exception ex)
{
_logger.LogWarning($"ConfigHelper Warning: You might be missing keys in appsettings.json, Message: ${ex.Message}");
return defaultValue;
}
}
private string CheckString(string configName, string defaultValue = "")
{
try
{
var configValue = _config[configName] ?? defaultValue;
return configValue;
} catch(Exception ex)
{
_logger.LogWarning($"ConfigHelper Warning: You might be missing keys in appsettings.json, Message: ${ex.Message}");
return defaultValue;
}
}
public UserConfig GetUserConfig(ClaimsPrincipal user)
{
var serverConfig = new UserConfig
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
PreferredGasMileageUnit = _config[nameof(UserConfig.PreferredGasMileageUnit)],
PreferredGasUnit = _config[nameof(UserConfig.PreferredGasUnit)],
UserLanguage = _config[nameof(UserConfig.UserLanguage)],
HideSoldVehicles = bool.Parse(_config[nameof(UserConfig.HideSoldVehicles)]),
EnableShopSupplies = bool.Parse(_config[nameof(UserConfig.EnableShopSupplies)]),
EnableExtraFieldColumns = bool.Parse(_config[nameof(UserConfig.EnableExtraFieldColumns)]),
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>(),
EnableCsvImports = CheckBool(CheckString(nameof(UserConfig.EnableCsvImports)), true),
UseDarkMode = CheckBool(CheckString(nameof(UserConfig.UseDarkMode))),
UseSystemColorMode = CheckBool(CheckString(nameof(UserConfig.UseSystemColorMode))),
UseMPG = CheckBool(CheckString(nameof(UserConfig.UseMPG)), true),
UseDescending = CheckBool(CheckString(nameof(UserConfig.UseDescending))),
EnableAuth = CheckBool(CheckString(nameof(UserConfig.EnableAuth))),
EnableRootUserOIDC = CheckBool(CheckString(nameof(UserConfig.EnableRootUserOIDC))),
HideZero = CheckBool(CheckString(nameof(UserConfig.HideZero))),
AutomaticDecimalFormat = CheckBool(CheckString(nameof(UserConfig.AutomaticDecimalFormat))),
UseUKMPG = CheckBool(CheckString(nameof(UserConfig.UseUKMPG))),
UseMarkDownOnSavedNotes = CheckBool(CheckString(nameof(UserConfig.UseMarkDownOnSavedNotes))),
UseThreeDecimalGasCost = CheckBool(CheckString(nameof(UserConfig.UseThreeDecimalGasCost)), true),
UseThreeDecimalGasConsumption = CheckBool(CheckString(nameof(UserConfig.UseThreeDecimalGasConsumption)), true),
EnableAutoReminderRefresh = CheckBool(CheckString(nameof(UserConfig.EnableAutoReminderRefresh))),
EnableAutoOdometerInsert = CheckBool(CheckString(nameof(UserConfig.EnableAutoOdometerInsert))),
PreferredGasMileageUnit = CheckString(nameof(UserConfig.PreferredGasMileageUnit)),
PreferredGasUnit = CheckString(nameof(UserConfig.PreferredGasUnit)),
UseUnitForFuelCost = CheckBool(CheckString(nameof(UserConfig.UseUnitForFuelCost))),
UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"),
HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))),
EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))),
EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))),
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>() ?? new UserConfig().VisibleTabs,
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>() ?? new UserConfig().TabOrder,
UserColumnPreferences = _config.GetSection(nameof(UserConfig.UserColumnPreferences)).Get<List<UserColumnPreference>>() ?? new List<UserColumnPreference>(),
ReminderUrgencyConfig = _config.GetSection(nameof(UserConfig.ReminderUrgencyConfig)).Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig(),
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
DefaultTab = (ImportMode)int.Parse(CheckString(nameof(UserConfig.DefaultTab), "8")),
DefaultReminderEmail = CheckString(nameof(UserConfig.DefaultReminderEmail)),
DisableRegistration = CheckBool(CheckString(nameof(UserConfig.DisableRegistration)))
};
int userId = 0;
if (user != null)

View File

@@ -16,6 +16,10 @@ namespace CarCareTracker.Helper
int ClearTempFolder();
int ClearUnlinkedThumbnails(List<string> linkedImages);
int ClearUnlinkedDocuments(List<string> linkedDocuments);
string GetWidgets();
bool WidgetsExist();
bool SaveWidgets(string widgetsData);
bool DeleteWidgets();
}
public class FileHelper : IFileHelper
{
@@ -30,7 +34,7 @@ namespace CarCareTracker.Helper
}
public List<string> GetLanguages()
{
var languagePath = Path.Combine(_webEnv.WebRootPath, "translations");
var languagePath = Path.Combine(_webEnv.ContentRootPath, "data", "translations");
var defaultList = new List<string>() { "en_US" };
if (Directory.Exists(languagePath))
{
@@ -68,7 +72,7 @@ namespace CarCareTracker.Helper
{
currentFilePath = currentFilePath.Substring(1);
}
string oldFilePath = Path.Combine(_webEnv.WebRootPath, currentFilePath);
string oldFilePath = currentFilePath.StartsWith("defaults/") ? Path.Combine(_webEnv.WebRootPath, currentFilePath) : Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath);
if (File.Exists(oldFilePath))
{
return oldFilePath;
@@ -90,7 +94,7 @@ namespace CarCareTracker.Helper
}
try
{
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{Guid.NewGuid()}");
var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{Guid.NewGuid()}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
//extract zip file
@@ -100,10 +104,11 @@ namespace CarCareTracker.Helper
var documentPath = Path.Combine(tempPath, "documents");
var translationPath = Path.Combine(tempPath, "translations");
var dataPath = Path.Combine(tempPath, StaticHelper.DbName);
var configPath = Path.Combine(tempPath, StaticHelper.UserConfigPath);
var widgetPath = Path.Combine(tempPath, StaticHelper.AdditionalWidgetsPath);
var configPath = Path.Combine(tempPath, StaticHelper.LegacyUserConfigPath);
if (Directory.Exists(imagePath))
{
var existingPath = Path.Combine(_webEnv.WebRootPath, "images");
var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "images");
if (!Directory.Exists(existingPath))
{
Directory.CreateDirectory(existingPath);
@@ -125,7 +130,7 @@ namespace CarCareTracker.Helper
}
if (Directory.Exists(documentPath))
{
var existingPath = Path.Combine(_webEnv.WebRootPath, "documents");
var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents");
if (!Directory.Exists(existingPath))
{
Directory.CreateDirectory(existingPath);
@@ -147,7 +152,7 @@ namespace CarCareTracker.Helper
}
if (Directory.Exists(translationPath))
{
var existingPath = Path.Combine(_webEnv.WebRootPath, "translations");
var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations");
if (!Directory.Exists(existingPath))
{
Directory.CreateDirectory(existingPath);
@@ -174,12 +179,16 @@ namespace CarCareTracker.Helper
//data path will always exist as it is created on startup if not.
File.Move(dataPath, StaticHelper.DbName, true);
}
if (File.Exists(widgetPath))
{
File.Move(widgetPath, StaticHelper.AdditionalWidgetsPath, true);
}
if (File.Exists(configPath))
{
//check if config folder exists.
if (!Directory.Exists("config/"))
if (!Directory.Exists("data/config"))
{
Directory.CreateDirectory("config/");
Directory.CreateDirectory("data/config");
}
File.Move(configPath, StaticHelper.UserConfigPath, true);
}
@@ -194,7 +203,7 @@ namespace CarCareTracker.Helper
public string MakeAttachmentsExport(List<GenericReportModel> exportData)
{
var folderName = Guid.NewGuid();
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{folderName}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
int fileIndex = 0;
@@ -218,11 +227,12 @@ namespace CarCareTracker.Helper
public string MakeBackup()
{
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
var imagePath = Path.Combine(_webEnv.WebRootPath, "images");
var documentPath = Path.Combine(_webEnv.WebRootPath, "documents");
var translationPath = Path.Combine(_webEnv.WebRootPath, "translations");
var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{folderName}");
var imagePath = Path.Combine(_webEnv.ContentRootPath, "data", "images");
var documentPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents");
var translationPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations");
var dataPath = StaticHelper.DbName;
var widgetPath = StaticHelper.AdditionalWidgetsPath;
var configPath = StaticHelper.UserConfigPath;
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
@@ -262,6 +272,12 @@ namespace CarCareTracker.Helper
Directory.CreateDirectory(newPath);
File.Copy(dataPath, $"{newPath}/{Path.GetFileName(dataPath)}");
}
if (File.Exists(widgetPath))
{
var newPath = Path.Combine(tempPath, "data");
Directory.CreateDirectory(newPath);
File.Copy(widgetPath, $"{newPath}/{Path.GetFileName(widgetPath)}");
}
if (File.Exists(configPath))
{
var newPath = Path.Combine(tempPath, "config");
@@ -285,8 +301,8 @@ namespace CarCareTracker.Helper
{
currentFilePath = currentFilePath.Substring(1);
}
string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder);
string oldFilePath = Path.Combine(_webEnv.WebRootPath, currentFilePath);
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", newFolder);
string oldFilePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
string newFileUploadPath = oldFilePath.Replace(tempPath, newFolder);
@@ -303,7 +319,7 @@ namespace CarCareTracker.Helper
{
currentFilePath = currentFilePath.Substring(1);
}
string filePath = Path.Combine(_webEnv.WebRootPath, currentFilePath);
string filePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath);
if (File.Exists(filePath))
{
File.Delete(filePath);
@@ -323,12 +339,20 @@ namespace CarCareTracker.Helper
var tempPath = GetFullFilePath("temp", false);
if (Directory.Exists(tempPath))
{
//delete files
var files = Directory.GetFiles(tempPath);
foreach (var file in files)
{
File.Delete(file);
filesDeleted++;
}
//delete folders
var folders = Directory.GetDirectories(tempPath);
foreach(var folder in folders)
{
Directory.Delete(folder, true);
filesDeleted++;
}
}
return filesDeleted;
}
@@ -368,5 +392,57 @@ namespace CarCareTracker.Helper
}
return filesDeleted;
}
public string GetWidgets()
{
if (File.Exists(StaticHelper.AdditionalWidgetsPath))
{
try
{
//read file
var widgets = File.ReadAllText(StaticHelper.AdditionalWidgetsPath);
return widgets;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return string.Empty;
}
}
return string.Empty;
}
public bool WidgetsExist()
{
return File.Exists(StaticHelper.AdditionalWidgetsPath);
}
public bool SaveWidgets(string widgetsData)
{
try
{
//Delete Widgets if exists
DeleteWidgets();
File.WriteAllText(StaticHelper.AdditionalWidgetsPath, widgetsData);
return true;
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteWidgets()
{
try
{
if (File.Exists(StaticHelper.AdditionalWidgetsPath))
{
File.Delete(StaticHelper.AdditionalWidgetsPath);
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}

View File

@@ -57,6 +57,10 @@ namespace CarCareTracker.Helper
if (i > 0)
{
var deltaMileage = currentObject.Mileage - previousMileage;
if (deltaMileage < 0)
{
deltaMileage = 0;
}
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,
@@ -72,7 +76,8 @@ namespace CarCareTracker.Helper
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes,
Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields
ExtraFields = currentObject.ExtraFields,
Files = currentObject.Files
};
if (currentObject.MissedFuelUp)
{
@@ -82,9 +87,9 @@ namespace CarCareTracker.Helper
unFactoredConsumption = 0;
unFactoredMileage = 0;
}
else if (currentObject.IsFillToFull)
else if (currentObject.IsFillToFull && currentObject.Mileage != default)
{
//if user filled to full.
//if user filled to full and an odometer is provided, otherwise we will defer calculations
if (convertedConsumption > 0.00M && deltaMileage > 0)
{
try
@@ -126,10 +131,14 @@ namespace CarCareTracker.Helper
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes,
Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields
ExtraFields = currentObject.ExtraFields,
Files = currentObject.Files
});
}
previousMileage = currentObject.Mileage;
if (currentObject.Mileage != default)
{
previousMileage = currentObject.Mileage;
}
}
return computedResults;
}

View File

@@ -1,6 +1,7 @@
using CarCareTracker.Models;
using MimeKit;
using MailKit.Net.Smtp;
using MailKit.Security;
namespace CarCareTracker.Helper
{
@@ -17,12 +18,12 @@ namespace CarCareTracker.Helper
private readonly IFileHelper _fileHelper;
private readonly ILogger<MailHelper> _logger;
public MailHelper(
IConfiguration config,
IConfigHelper config,
IFileHelper fileHelper,
ILogger<MailHelper> logger
) {
//load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>() ?? new MailConfig();
mailConfig = config.GetMailConfig();
_fileHelper = fileHelper;
_logger = logger;
}
@@ -30,100 +31,106 @@ namespace CarCareTracker.Helper
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
return OperationResponse.Failed("SMTP Server Not Setup");
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) {
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
return OperationResponse.Failed("Email Address or Token is invalid");
}
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(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
return OperationResponse.Succeed("Email Sent!");
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
}
public OperationResponse NotifyUserForPasswordReset(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
return OperationResponse.Failed("SMTP Server Not Setup");
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token))
{
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
return OperationResponse.Failed("Email Address or Token is invalid");
}
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(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
return OperationResponse.Succeed("Email Sent!");
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
}
public OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
return OperationResponse.Failed("SMTP Server Not Setup");
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token))
{
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
return OperationResponse.Failed("Email Address or Token is invalid");
}
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(new List<string> { emailAddress}, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
return OperationResponse.Succeed("Email Sent!");
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
}
public OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
return OperationResponse.Failed("SMTP Server Not Setup");
}
if (!emailAddresses.Any())
{
return new OperationResponse { Success = false, Message = "No recipients could be found" };
return OperationResponse.Failed("No recipients could be found");
}
if (!reminders.Any())
{
return new OperationResponse { Success = false, Message = "No reminders could be found" };
return OperationResponse.Failed("No reminders could be found");
}
//get email template, this file has to exist since it's a static file.
var emailTemplatePath = _fileHelper.GetFullFilePath(StaticHelper.ReminderEmailTemplate);
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
//construct html table.
string emailBody = File.ReadAllText(emailTemplatePath);
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}");
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}");
string tableBody = "";
foreach(ReminderRecordViewModel reminder in reminders)
{
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
tableBody += $"<tr class='{reminder.Urgency}'><td>{StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency)}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
}
emailBody = emailBody.Replace("{TableBody}", tableBody);
try
{
SendEmail(emailAddresses, emailSubject, emailBody);
return new OperationResponse { Success = true, Message = "Email Sent!" };
var result = SendEmail(emailAddresses, emailSubject, emailBody);
if (result)
{
return OperationResponse.Succeed("Email Sent!");
} else
{
return OperationResponse.Failed();
}
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
return OperationResponse.Failed(ex.Message);
}
}
private bool SendEmail(List<string> emailTo, string emailSubject, string emailBody) {
@@ -145,7 +152,7 @@ namespace CarCareTracker.Helper
using (var client = new SmtpClient())
{
client.Connect(server, mailConfig.Port, MailKit.Security.SecureSocketOptions.Auto);
client.Connect(server, mailConfig.Port, 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)) {

View File

@@ -66,6 +66,10 @@ namespace CarCareTracker.Helper
var reminderUrgencyConfig = _config.GetReminderUrgencyConfig();
foreach (var reminder in reminders)
{
if (reminder.UseCustomThresholds)
{
reminderUrgencyConfig = reminder.CustomThresholds;
}
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,
@@ -75,6 +79,7 @@ namespace CarCareTracker.Helper
Description = reminder.Description,
Notes = reminder.Notes,
Metric = reminder.Metric,
UserMetric = reminder.Metric,
IsRecurring = reminder.IsRecurring,
Tags = reminder.Tags
};

View File

@@ -5,93 +5,166 @@ namespace CarCareTracker.Helper
{
public interface IReportHelper
{
IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0, bool sortIntoYear = false);
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0, bool sortIntoYear = false);
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0, bool sortIntoYear = false);
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0, bool sortIntoYear = false);
IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0, bool sortIntoYear = false);
IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0, bool sortIntoYear = false);
}
public class ReportHelper: IReportHelper
{
public IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
odometerRecords.RemoveAll(x => x.Date.Year != year);
}
return odometerRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = 0,
DistanceTraveled = x.Sum(y=>y.DistanceTraveled)
});
return odometerRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = 0,
DistanceTraveled = x.Sum(y => y.DistanceTraveled)
});
} else
{
return odometerRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = 0,
DistanceTraveled = x.Sum(y => y.DistanceTraveled)
});
}
}
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
serviceRecords.RemoveAll(x => x.Date.Year != year);
}
return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
return serviceRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = x.Sum(y => y.Cost)
});
} else
{
return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
repairRecords.RemoveAll(x => x.Date.Year != year);
}
return repairRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
return repairRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = x.Sum(y => y.Cost)
});
} else
{
return repairRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
upgradeRecords.RemoveAll(x => x.Date.Year != year);
}
return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
return upgradeRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = x.Sum(y => y.Cost)
});
} else
{
return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
gasRecords.RemoveAll(x => x.Date.Year != year);
}
return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
return gasRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = x.Sum(y => y.Cost)
});
} else
{
return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0)
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0, bool sortIntoYear = false)
{
if (year != default)
{
taxRecords.RemoveAll(x => x.Date.Year != year);
}
return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
if (sortIntoYear)
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
return taxRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key.Month,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month),
Year = x.Key.Year,
Cost = x.Sum(y => y.Cost)
});
} else
{
return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
}
}

View File

@@ -1,6 +1,9 @@
using CarCareTracker.Models;
using CsvHelper;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace CarCareTracker.Helper
{
@@ -9,13 +12,18 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.3.6";
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public static string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public const string VersionNumber = "1.4.3";
public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "data/config/userConfig.json";
public const string LegacyUserConfigPath = "config/userConfig.json";
public const string AdditionalWidgetsPath = "data/widgets.html";
public const string GenericErrorMessage = "An error occurred, please try again later";
public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public const string TranslationPath = "https://hargata.github.io/lubelog_translations";
public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
{
switch (input)
@@ -30,6 +38,66 @@ namespace CarCareTracker.Helper
return input.ToString();
}
}
public static string GetTitleCaseReminderUrgency(string input)
{
switch (input)
{
case "NotUrgent":
return "Not Urgent";
case "VeryUrgent":
return "Very Urgent";
case "PastDue":
return "Past Due";
default:
return input;
}
}
public static string GetReminderUrgencyColor(ReminderUrgency input)
{
switch (input)
{
case ReminderUrgency.NotUrgent:
return "text-bg-success";
case ReminderUrgency.VeryUrgent:
return "text-bg-danger";
case ReminderUrgency.PastDue:
return "text-bg-secondary";
default:
return "text-bg-warning";
}
}
public static string GetPlanRecordColor(PlanPriority input)
{
switch (input)
{
case PlanPriority.Critical:
return "text-bg-danger";
case PlanPriority.Normal:
return "text-bg-primary";
case PlanPriority.Low:
return "text-bg-info";
default:
return "text-bg-primary";
}
}
public static string GetPlanRecordProgress(PlanProgress input)
{
switch (input)
{
case PlanProgress.Backlog:
return "Planned";
case PlanProgress.InProgress:
return "Doing";
case PlanProgress.Testing:
return "Testing";
case PlanProgress.Done:
return "Done";
default:
return input.ToString();
}
}
public static string TruncateStrings(string input, int maxLength = 25)
{
@@ -119,6 +187,10 @@ namespace CarCareTracker.Helper
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
};
}
public static List<string> GetBarChartColors()
{
return new List<string> { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
}
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
{
@@ -171,7 +243,8 @@ namespace CarCareTracker.Helper
public static List<ExtraField> AddExtraFields(List<ExtraField> recordExtraFields, List<ExtraField> templateExtraFields)
{
if (!templateExtraFields.Any()) {
if (!templateExtraFields.Any())
{
return new List<ExtraField>();
}
if (!recordExtraFields.Any())
@@ -192,13 +265,15 @@ namespace CarCareTracker.Helper
extraField.IsRequired = templateExtraFields.Where(x => x.Name == extraField.Name).First().IsRequired;
}
//append extra fields
foreach(ExtraField extraField in templateExtraFields)
foreach (ExtraField extraField in templateExtraFields)
{
if (!recordFieldNames.Contains(extraField.Name))
{
recordExtraFields.Add(extraField);
}
}
//re-order extra fields
recordExtraFields = recordExtraFields.OrderBy(x => templateExtraFields.FindIndex(y => y.Name == x.Name)).ToList();
return recordExtraFields;
}
@@ -238,27 +313,122 @@ namespace CarCareTracker.Helper
if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
Console.WriteLine($"SMTP Configured for {mailConfig.EmailServer}");
} else
}
else
{
Console.WriteLine("SMTP Not Configured");
}
var motd = config["LUBELOGGER_MOTD"] ?? "Not Configured";
Console.WriteLine($"Message Of The Day: {motd}");
if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name))
{
Console.WriteLine("WARNING: No Locale or Culture Configured for LubeLogger, Check Environment Variables");
}
//Create folders if they don't exist.
if (!Directory.Exists("data"))
{
Directory.CreateDirectory("data");
Console.WriteLine("Created data directory");
}
if (!Directory.Exists("data/images"))
{
Console.WriteLine("Created images directory");
Directory.CreateDirectory("data/images");
}
if (!Directory.Exists("data/documents"))
{
Directory.CreateDirectory("data/documents");
Console.WriteLine("Created documents directory");
}
if (!Directory.Exists("data/translations"))
{
Directory.CreateDirectory("data/translations");
Console.WriteLine("Created translations directory");
}
if (!Directory.Exists("data/temp"))
{
Directory.CreateDirectory("data/temp");
Console.WriteLine("Created translations directory");
}
if (!Directory.Exists("data/config"))
{
Directory.CreateDirectory("data/config");
Console.WriteLine("Created config directory");
}
}
public static async void NotifyAsync(string webhookURL, int vehicleId, string username, string action)
public static void CheckMigration(string webRootPath, string webContentPath)
{
//check if current working directory differs from content root.
if (Directory.GetCurrentDirectory() != webContentPath)
{
Console.WriteLine("WARNING: The Working Directory differs from the Web Content Path");
Console.WriteLine($"Working Directory: {Directory.GetCurrentDirectory()}");
Console.WriteLine($"Web Content Path: {webContentPath}");
}
//migrates all user-uploaded files from webroot to new data folder
//images
var imagePath = Path.Combine(webRootPath, "images");
var docsPath = Path.Combine(webRootPath, "documents");
var translationPath = Path.Combine(webRootPath, "translations");
var tempPath = Path.Combine(webRootPath, "temp");
if (File.Exists(LegacyUserConfigPath))
{
File.Move(LegacyUserConfigPath, UserConfigPath, true);
}
if (Directory.Exists(imagePath))
{
foreach (string fileToMove in Directory.GetFiles(imagePath))
{
var newFilePath = $"data/images/{Path.GetFileName(fileToMove)}";
File.Move(fileToMove, newFilePath, true);
Console.WriteLine($"Migrated Image: {Path.GetFileName(fileToMove)}");
}
}
if (Directory.Exists(docsPath))
{
foreach (string fileToMove in Directory.GetFiles(docsPath))
{
var newFilePath = $"data/documents/{Path.GetFileName(fileToMove)}";
File.Move(fileToMove, newFilePath, true);
Console.WriteLine($"Migrated Document: {Path.GetFileName(fileToMove)}");
}
}
if (Directory.Exists(translationPath))
{
foreach (string fileToMove in Directory.GetFiles(translationPath))
{
var newFilePath = $"data/translations/{Path.GetFileName(fileToMove)}";
File.Move(fileToMove, newFilePath, true);
Console.WriteLine($"Migrated Translation: {Path.GetFileName(fileToMove)}");
}
}
if (Directory.Exists(tempPath))
{
foreach (string fileToMove in Directory.GetFiles(tempPath))
{
var newFilePath = $"data/temp/{Path.GetFileName(fileToMove)}";
File.Move(fileToMove, newFilePath, true);
Console.WriteLine($"Migrated Temp File: {Path.GetFileName(fileToMove)}");
}
}
}
public static async void NotifyAsync(string webhookURL, WebHookPayload webHookPayload)
{
if (string.IsNullOrWhiteSpace(webhookURL))
{
return;
}
var httpClient = new HttpClient();
var httpParams = new Dictionary<string, string>
{
{ "vehicleId", vehicleId.ToString() },
{ "username", username },
{ "action", action },
};
httpClient.PostAsJsonAsync(webhookURL, httpParams);
if (webhookURL.StartsWith("discord://"))
{
webhookURL = webhookURL.Replace("discord://", "https://"); //cleanurl
//format to discord
httpClient.PostAsJsonAsync(webhookURL, DiscordWebHook.FromWebHookPayload(webHookPayload));
}
else
{
httpClient.PostAsJsonAsync(webhookURL, webHookPayload);
}
}
public static string GetImportModeIcon(ImportMode importMode)
{
@@ -288,6 +458,90 @@ namespace CarCareTracker.Helper
return "bi-file-bar-graph";
}
}
public static string GetVehicleIdentifier(Vehicle vehicle)
{
if (vehicle.VehicleIdentifier == "LicensePlate")
{
return vehicle.LicensePlate;
}
else
{
if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier))
{
return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value;
}
else
{
return "N/A";
}
}
}
public static string GetVehicleIdentifier(VehicleViewModel vehicle)
{
if (vehicle.VehicleIdentifier == "LicensePlate")
{
return vehicle.LicensePlate;
}
else
{
if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier))
{
return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value;
}
else
{
return "N/A";
}
}
}
//Translations
public static string GetTranslationDownloadPath(string continent, string name)
{
if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name))
{
return string.Empty;
}
else
{
switch (continent)
{
case "NorthAmerica":
continent = "North America";
break;
case "SouthAmerica":
continent = "South America";
break;
}
return $"{TranslationPath}/{continent}/{name}.json";
}
}
public static string GetTranslationName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return string.Empty;
}
else
{
try
{
string cleanedName = name.Contains("_") ? name.Replace("_", "-") : name;
string displayName = CultureInfo.GetCultureInfo(cleanedName).DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
return name;
}
else
{
return displayName;
}
}
catch (Exception ex)
{
return name;
}
}
}
//CSV Write Methods
public static void WriteGenericRecordExportModel(CsvWriter _csv, IEnumerable<GenericRecordExportModel> genericRecords)
{
@@ -448,6 +702,35 @@ namespace CarCareTracker.Helper
_csv.NextRecord();
}
}
public static string HideZeroCost(string input, bool hideZero, string decorations = "")
{
if (input == 0M.ToString("C2") && hideZero)
{
return "---";
}
else
{
return string.IsNullOrWhiteSpace(decorations) ? input : $"{input}{decorations}";
}
}
public static string HideZeroCost(decimal input, bool hideZero, string decorations = "")
{
if (input == default && hideZero)
{
return "---";
}
else
{
return string.IsNullOrWhiteSpace(decorations) ? input.ToString("C2") : $"{input.ToString("C2")}{decorations}";
}
}
public static JsonSerializerOptions GetInvariantOption()
{
var serializerOption = new JsonSerializerOptions();
serializerOption.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
serializerOption.Converters.Add(new InvariantConverter());
return serializerOption;
}
public static void WriteGasRecordExportModel(CsvWriter _csv, IEnumerable<GasRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
@@ -485,5 +768,52 @@ namespace CarCareTracker.Helper
_csv.NextRecord();
}
}
public static byte[] RemindersToCalendar(List<ReminderRecordViewModel> reminders)
{
//converts reminders to iCal file
StringBuilder sb = new StringBuilder();
//start the calendar item
sb.AppendLine("BEGIN:VCALENDAR");
sb.AppendLine("VERSION:2.0");
sb.AppendLine("PRODID:lubelogger.com");
sb.AppendLine("CALSCALE:GREGORIAN");
sb.AppendLine("METHOD:PUBLISH");
//create events.
foreach(ReminderRecordViewModel reminder in reminders)
{
var dtStart = reminder.Date.Date.ToString("yyyyMMddTHHmm00");
var dtEnd = reminder.Date.Date.AddDays(1).AddMilliseconds(-1).ToString("yyyyMMddTHHmm00");
var calendarUID = new Guid(MD5.HashData(Encoding.UTF8.GetBytes($"{dtStart}_{reminder.Description}")));
sb.AppendLine("BEGIN:VEVENT");
sb.AppendLine("DTSTAMP:" + DateTime.Now.ToString("yyyyMMddTHHmm00"));
sb.AppendLine("UID:" + calendarUID);
sb.AppendLine("DTSTART:" + dtStart);
sb.AppendLine("DTEND:" + dtEnd);
sb.AppendLine($"SUMMARY:{reminder.Description}");
sb.AppendLine($"DESCRIPTION:{reminder.Description}");
switch (reminder.Urgency)
{
case ReminderUrgency.NotUrgent:
sb.AppendLine("PRIORITY:3");
break;
case ReminderUrgency.Urgent:
sb.AppendLine("PRIORITY:2");
break;
case ReminderUrgency.VeryUrgent:
sb.AppendLine("PRIORITY:1");
break;
case ReminderUrgency.PastDue:
sb.AppendLine("PRIORITY:1");
break;
}
sb.AppendLine("END:VEVENT");
}
//end calendar item
sb.AppendLine("END:VCALENDAR");
string calendarContent = sb.ToString();
return Encoding.UTF8.GetBytes(calendarContent);
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Caching.Memory;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
namespace CarCareTracker.Helper
@@ -6,17 +7,22 @@ namespace CarCareTracker.Helper
public interface ITranslationHelper
{
string Translate(string userLanguage, string text);
Dictionary<string, string> GetTranslations(string userLanguage);
OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations);
string ExportTranslation(Dictionary<string, string> translations);
}
public class TranslationHelper : ITranslationHelper
{
private readonly IFileHelper _fileHelper;
private readonly IConfiguration _config;
private readonly ILogger<ITranslationHelper> _logger;
private IMemoryCache _cache;
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache)
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache, ILogger<ITranslationHelper> logger)
{
_fileHelper = fileHelper;
_config = config;
_cache = memoryCache;
_logger = logger;
}
public string Translate(string userLanguage, string text)
{
@@ -36,11 +42,13 @@ namespace CarCareTracker.Helper
return translationDictionary ?? new Dictionary<string, string>();
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
else
{
_logger.LogError($"Could not find translation file for {userLanguage}");
return new Dictionary<string, string>();
}
});
@@ -52,10 +60,138 @@ namespace CarCareTracker.Helper
{
//create entry
dictionary.Add(translationKey, text);
_logger.LogInformation($"Translation key added to {userLanguage} for {translationKey} with value {text}");
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
return text;
}
return text;
}
private Dictionary<string, string> GetDefaultTranslation()
{
//this method always returns en_US translation.
var translationFilePath = _fileHelper.GetFullFilePath($"/defaults/en_US.json");
if (!string.IsNullOrWhiteSpace(translationFilePath))
{
//file exists.
try
{
var translationFile = File.ReadAllText(translationFilePath);
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
return translationDictionary ?? new Dictionary<string, string>();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
_logger.LogError($"Could not find translation file for en_US");
return new Dictionary<string, string>();
}
public Dictionary<string, string> GetTranslations(string userLanguage)
{
var defaultTranslation = GetDefaultTranslation();
if (userLanguage == "en_US")
{
return defaultTranslation;
}
var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json");
if (!string.IsNullOrWhiteSpace(translationFilePath))
{
//file exists.
try
{
var translationFile = File.ReadAllText(translationFilePath);
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
if (translationDictionary != null)
{
foreach(var translation in translationDictionary)
{
if (defaultTranslation.ContainsKey(translation.Key))
{
defaultTranslation[translation.Key] = translation.Value;
}
}
}
return defaultTranslation ?? new Dictionary<string, string>();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
_logger.LogError($"Could not find translation file for {userLanguage}");
return new Dictionary<string, string>();
}
public OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations)
{
bool create = bool.Parse(_config["LUBELOGGER_TRANSLATOR"] ?? "false");
bool isDefaultLanguage = userLanguage == "en_US";
if (isDefaultLanguage && !create)
{
return OperationResponse.Failed("The translation file name en_US is reserved.");
}
if (string.IsNullOrWhiteSpace(userLanguage))
{
return OperationResponse.Failed("File name is not provided.");
}
if (!translations.Any())
{
return OperationResponse.Failed("Translation has no data.");
}
var translationFilePath = isDefaultLanguage ? _fileHelper.GetFullFilePath($"/defaults/en_US.json") : _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false);
try
{
if (File.Exists(translationFilePath))
{
//write to file
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
_cache.Remove($"lang_{userLanguage}"); //clear out cache, force a reload from file.
} else
{
//check if directory exists first.
var translationDirectory = _fileHelper.GetFullFilePath("translations/", false);
if (!Directory.Exists(translationDirectory))
{
Directory.CreateDirectory(translationDirectory);
}
//write to file
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
}
return OperationResponse.Succeed("Translation Updated");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return OperationResponse.Failed();
}
}
public string ExportTranslation(Dictionary<string, string> translations)
{
try
{
var tempFileName = $"/temp/{Guid.NewGuid()}.json";
string uploadDirectory = _fileHelper.GetFullFilePath("temp/", false);
if (!Directory.Exists(uploadDirectory))
{
Directory.CreateDirectory(uploadDirectory);
}
var saveFilePath = _fileHelper.GetFullFilePath(tempFileName, false);
//standardize translation format for export only.
Dictionary<string, string> sortedTranslations = new Dictionary<string, string>();
foreach (var translation in translations.OrderBy(x => x.Key))
{
sortedTranslations.Add(translation.Key, translation.Value);
};
File.WriteAllText(saveFilePath, JsonSerializer.Serialize(sortedTranslations, new JsonSerializerOptions { WriteIndented = true }));
return tempFileName;
}
catch(Exception ex)
{
_logger.LogError(ex.Message);
return string.Empty;
}
}
}
}

View File

@@ -71,13 +71,13 @@ namespace CarCareTracker.Logic
var existingUser = _userData.GetUserRecordById(userId);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Invalid user" };
return OperationResponse.Failed("Invalid user");
}
//validate user token
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != existingUser.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
return OperationResponse.Failed("Invalid Token");
}
if (!string.IsNullOrWhiteSpace(credentials.UserName) && existingUser.UserName != credentials.UserName)
{
@@ -85,7 +85,7 @@ namespace CarCareTracker.Logic
var existingUserWithUserName = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUserWithUserName.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
return OperationResponse.Failed("Username already taken");
}
existingUser.UserName = credentials.UserName;
}
@@ -95,7 +95,7 @@ namespace CarCareTracker.Logic
var existingUserWithEmailAddress = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmailAddress.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
return OperationResponse.Failed("A user with that email already exists");
}
existingUser.EmailAddress = credentials.EmailAddress;
}
@@ -107,7 +107,7 @@ namespace CarCareTracker.Logic
//delete token
_tokenData.DeleteToken(existingToken.Id);
var result = _userData.SaveUserRecord(existingUser);
return new OperationResponse { Success = result, Message = result ? "User Updated" : StaticHelper.GenericErrorMessage };
return OperationResponse.Conditional(result, "User Updated", string.Empty);
}
public OperationResponse RegisterOpenIdUser(LoginModel credentials)
{
@@ -115,21 +115,21 @@ namespace CarCareTracker.Logic
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
return OperationResponse.Failed("Invalid Token");
}
if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName))
{
return new OperationResponse { Success = false, Message = "Username cannot be blank" };
return OperationResponse.Failed("Username cannot be blank");
}
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
return OperationResponse.Failed("Username already taken");
}
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmail.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
return OperationResponse.Failed("A user with that email already exists");
}
_tokenData.DeleteToken(existingToken.Id);
var newUser = new UserData()
@@ -141,11 +141,11 @@ namespace CarCareTracker.Logic
var result = _userData.SaveUserRecord(newUser);
if (result)
{
return new OperationResponse { Success = true, Message = "You will be logged in briefly." };
return OperationResponse.Succeed("You will be logged in briefly.");
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
return OperationResponse.Failed("Something went wrong, please try again later.");
}
}
//handles user registration
@@ -155,22 +155,22 @@ namespace CarCareTracker.Logic
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
return OperationResponse.Failed("Invalid Token");
}
//token is valid, check if username and password is acceptable and that username is unique.
if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName) || string.IsNullOrWhiteSpace(credentials.Password))
{
return new OperationResponse { Success = false, Message = "Neither username nor password can be blank" };
return OperationResponse.Failed("Neither username nor password can be blank");
}
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
return OperationResponse.Failed("Username already taken");
}
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmail.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
return OperationResponse.Failed("A user with that email already exists");
}
//username is unique then we delete the token and create the user.
_tokenData.DeleteToken(existingToken.Id);
@@ -183,11 +183,11 @@ namespace CarCareTracker.Logic
var result = _userData.SaveUserRecord(newUser);
if (result)
{
return new OperationResponse { Success = true, Message = "You will be redirected to the login page briefly." };
return OperationResponse.Succeed("You will be redirected to the login page briefly.");
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
return OperationResponse.Failed();
}
}
/// <summary>
@@ -205,24 +205,24 @@ namespace CarCareTracker.Logic
}
//for security purposes we want to always return true for this method.
//otherwise someone can spam the reset password method to sniff out users.
return new OperationResponse { Success = true, Message = "If your user exists in the system you should receive an email shortly with instructions on how to proceed." };
return OperationResponse.Succeed("If your user exists in the system you should receive an email shortly with instructions on how to proceed.");
}
public OperationResponse ResetPasswordByUser(LoginModel credentials)
{
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
return OperationResponse.Failed("Invalid Token");
}
if (string.IsNullOrWhiteSpace(credentials.Password))
{
return new OperationResponse { Success = false, Message = "New Password cannot be blank" };
return OperationResponse.Failed("New Password cannot be blank");
}
//if token is valid.
var existingUser = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Unable to locate user" };
return OperationResponse.Failed("Unable to locate user");
}
existingUser.Password = GetHash(credentials.Password);
var result = _userData.SaveUserRecord(existingUser);
@@ -230,10 +230,10 @@ namespace CarCareTracker.Logic
_tokenData.DeleteToken(existingToken.Id);
if (result)
{
return new OperationResponse { Success = true, Message = "Password resetted, you will be redirected to login page shortly." };
return OperationResponse.Succeed("Password resetted, you will be redirected to login page shortly.");
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
}
/// <summary>
@@ -245,14 +245,7 @@ namespace CarCareTracker.Logic
{
if (UserIsRoot(credentials))
{
return new UserData()
{
Id = -1,
UserName = credentials.UserName,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
return GetRootUserData(credentials.UserName);
}
else
{
@@ -271,6 +264,13 @@ namespace CarCareTracker.Logic
}
public UserData ValidateOpenIDUser(LoginModel credentials)
{
//validate for root user
var isRootUser = _configHelper.AuthenticateRootUserOIDC(credentials.EmailAddress);
if (isRootUser)
{
return GetRootUserData(credentials.EmailAddress);
}
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (result.Id != default)
{
@@ -310,7 +310,7 @@ namespace CarCareTracker.Logic
var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress);
if (existingToken.Id != default)
{
return new OperationResponse { Success = false, Message = "There is an existing token tied to this email address" };
return OperationResponse.Failed("There is an existing token tied to this email address");
}
var token = new Token()
{
@@ -323,16 +323,16 @@ namespace CarCareTracker.Logic
result = _mailHelper.NotifyUserForRegistration(emailAddress, token.Body).Success;
if (!result)
{
return new OperationResponse { Success = false, Message = "Token Generated, but Email failed to send, please check your SMTP settings." };
return OperationResponse.Failed("Token Generated, but Email failed to send, please check your SMTP settings.");
}
}
if (result)
{
return new OperationResponse { Success = true, Message = "Token Generated!" };
return OperationResponse.Succeed("Token Generated!");
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
}
public bool DeleteUserToken(int tokenId)
@@ -351,18 +351,18 @@ namespace CarCareTracker.Logic
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Unable to find user" };
return OperationResponse.Failed("Unable to find user");
}
var newPassword = Guid.NewGuid().ToString().Substring(0, 8);
existingUser.Password = GetHash(newPassword);
var result = _userData.SaveUserRecord(existingUser);
if (result)
{
return new OperationResponse { Success = true, Message = newPassword };
return OperationResponse.Succeed(newPassword);
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
return OperationResponse.Failed();
}
}
#endregion
@@ -420,6 +420,17 @@ namespace CarCareTracker.Logic
var hashedPassword = GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
}
private UserData GetRootUserData(string username)
{
return new UserData()
{
Id = -1,
UserName = username,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
}
#endregion
private static string GetHash(string value)
{

View File

@@ -33,6 +33,10 @@ namespace CarCareTracker.Logic
}
public bool AutoInsertOdometerRecord(OdometerRecord odometer)
{
if (odometer.Mileage == default)
{
return false;
}
var lastReportedMileage = GetLastOdometerRecordMileage(odometer.VehicleId, new List<OdometerRecord>());
odometer.InitialMileage = lastReportedMileage != default ? lastReportedMileage : odometer.Mileage;

View File

@@ -51,16 +51,16 @@ namespace CarCareTracker.Logic
var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(existingUser.Id, vehicleId);
if (userAccess != null)
{
return new OperationResponse { Success = false, Message = "User is already a collaborator" };
return OperationResponse.Failed("User is already a collaborator");
}
var result = AddUserAccessToVehicle(existingUser.Id, vehicleId);
if (result)
{
return new OperationResponse { Success = true, Message = "Collaborator Added" };
return OperationResponse.Succeed("Collaborator Added");
}
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
return OperationResponse.Failed();
}
return new OperationResponse { Success = false, Message = $"Unable to find user {username} in the system" };
return OperationResponse.Failed($"Unable to find user {username} in the system");
}
public bool DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Controllers;
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
@@ -6,9 +7,19 @@ namespace CarCareTracker.Logic
{
public interface IVehicleLogic
{
VehicleRecords GetVehicleRecords(int vehicleId);
decimal GetVehicleTotalCost(VehicleRecords vehicleRecords);
int GetMaxMileage(int vehicleId);
int GetMaxMileage(VehicleRecords vehicleRecords);
int GetMinMileage(int vehicleId);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId);
int GetMinMileage(VehicleRecords vehicleRecords);
int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage);
List<VehicleInfo> GetVehicleInfo(List<Vehicle> vehicles);
List<ReminderRecordViewModel> GetReminders(List<Vehicle> vehicles, bool isCalendar);
List<PlanRecord> GetPlans(List<Vehicle> vehicles, bool excludeDone);
bool UpdateRecurringTaxes(int vehicleId);
void RestoreSupplyRecordsByUsage(List<SupplyUsageHistory> supplyUsage, string usageDescription);
}
public class VehicleLogic: IVehicleLogic
{
@@ -16,25 +27,62 @@ namespace CarCareTracker.Logic
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IPlanRecordDataAccess _planRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IVehicleDataAccess _dataAccess;
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
private readonly ILogger<VehicleLogic> _logger;
public VehicleLogic(
IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper
IPlanRecordDataAccess planRecordDataAccess,
IReminderHelper reminderHelper,
IVehicleDataAccess dataAccess,
ISupplyRecordDataAccess supplyRecordDataAccess,
ILogger<VehicleLogic> logger
) {
_serviceRecordDataAccess = serviceRecordDataAccess;
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_planRecordDataAccess = planRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
_dataAccess = dataAccess;
_supplyRecordDataAccess = supplyRecordDataAccess;
_logger = logger;
}
public VehicleRecords GetVehicleRecords(int vehicleId)
{
return new VehicleRecords
{
ServiceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId),
GasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId),
CollisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId),
TaxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId),
UpgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId),
OdometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId),
};
}
public decimal GetVehicleTotalCost(VehicleRecords vehicleRecords)
{
var serviceRecordSum = vehicleRecords.ServiceRecords.Sum(x => x.Cost);
var repairRecordSum = vehicleRecords.CollisionRecords.Sum(x => x.Cost);
var upgradeRecordSum = vehicleRecords.UpgradeRecords.Sum(x => x.Cost);
var taxRecordSum = vehicleRecords.TaxRecords.Sum(x => x.Cost);
var gasRecordSum = vehicleRecords.GasRecords.Sum(x => x.Cost);
return serviceRecordSum + repairRecordSum + upgradeRecordSum + taxRecordSum + gasRecordSum;
}
public int GetMaxMileage(int vehicleId)
{
@@ -66,6 +114,31 @@ namespace CarCareTracker.Logic
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMaxMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
if (vehicleRecords.ServiceRecords.Any())
{
numbersArray.Add(vehicleRecords.ServiceRecords.Max(x => x.Mileage));
}
if (vehicleRecords.CollisionRecords.Any())
{
numbersArray.Add(vehicleRecords.CollisionRecords.Max(x => x.Mileage));
}
if (vehicleRecords.GasRecords.Any())
{
numbersArray.Add(vehicleRecords.GasRecords.Max(x => x.Mileage));
}
if (vehicleRecords.UpgradeRecords.Any())
{
numbersArray.Add(vehicleRecords.UpgradeRecords.Max(x => x.Mileage));
}
if (vehicleRecords.OdometerRecords.Any())
{
numbersArray.Add(vehicleRecords.OdometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMinMileage(int vehicleId)
{
var numbersArray = new List<int>();
@@ -96,12 +169,257 @@ namespace CarCareTracker.Logic
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId)
public int GetMinMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
var _serviceRecords = vehicleRecords.ServiceRecords.Where(x => x.Mileage != default).ToList();
if (_serviceRecords.Any())
{
numbersArray.Add(_serviceRecords.Min(x => x.Mileage));
}
var _repairRecords = vehicleRecords.CollisionRecords.Where(x => x.Mileage != default).ToList();
if (_repairRecords.Any())
{
numbersArray.Add(_repairRecords.Min(x => x.Mileage));
}
var _gasRecords = vehicleRecords.GasRecords.Where(x => x.Mileage != default).ToList();
if (_gasRecords.Any())
{
numbersArray.Add(_gasRecords.Min(x => x.Mileage));
}
var _upgradeRecords = vehicleRecords.UpgradeRecords.Where(x => x.Mileage != default).ToList();
if (_upgradeRecords.Any())
{
numbersArray.Add(_upgradeRecords.Min(x => x.Mileage));
}
var _odometerRecords = vehicleRecords.OdometerRecords.Where(x => x.Mileage != default).ToList();
if (_odometerRecords.Any())
{
numbersArray.Add(_odometerRecords.Min(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords)
{
var startDate = DateTime.Now;
var endDate = DateTime.Now;
if (!string.IsNullOrWhiteSpace(soldDate))
{
endDate = DateTime.Parse(soldDate);
}
if (!string.IsNullOrWhiteSpace(purchaseDate))
{
//if purchase date is provided, then we just have to subtract the begin date to end date and return number of months
startDate = DateTime.Parse(purchaseDate);
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
}
var dateArray = new List<DateTime>();
dateArray.AddRange(serviceRecords.Select(x => x.Date));
dateArray.AddRange(repairRecords.Select(x => x.Date));
dateArray.AddRange(gasRecords.Select(x => x.Date));
dateArray.AddRange(upgradeRecords.Select(x => x.Date));
dateArray.AddRange(odometerRecords.Select(x => x.Date));
dateArray.AddRange(taxRecords.Select(x => x.Date));
if (dateArray.Any())
{
startDate = dateArray.Min();
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
} else
{
return 1;
}
}
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage)
{
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
return results.Any(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
}
public List<VehicleInfo> GetVehicleInfo(List<Vehicle> vehicles)
{
List<VehicleInfo> apiResult = new List<VehicleInfo>();
foreach (Vehicle vehicle in vehicles)
{
var currentMileage = GetMaxMileage(vehicle.Id);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id);
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id);
var planRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id);
var resultToAdd = new VehicleInfo()
{
VehicleData = vehicle,
LastReportedOdometer = currentMileage,
ServiceRecordCount = serviceRecords.Count(),
ServiceRecordCost = serviceRecords.Sum(x => x.Cost),
RepairRecordCount = repairRecords.Count(),
RepairRecordCost = repairRecords.Sum(x => x.Cost),
UpgradeRecordCount = upgradeRecords.Count(),
UpgradeRecordCost = upgradeRecords.Sum(x => x.Cost),
GasRecordCount = gasRecords.Count(),
GasRecordCost = gasRecords.Sum(x => x.Cost),
TaxRecordCount = taxRecords.Count(),
TaxRecordCost = taxRecords.Sum(x => x.Cost),
VeryUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.VeryUrgent),
PastDueReminderCount = results.Count(x => x.Urgency == ReminderUrgency.PastDue),
UrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.Urgent),
NotUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.NotUrgent),
PlanRecordBackLogCount = planRecords.Count(x => x.Progress == PlanProgress.Backlog),
PlanRecordInProgressCount = planRecords.Count(x => x.Progress == PlanProgress.InProgress),
PlanRecordTestingCount = planRecords.Count(x => x.Progress == PlanProgress.Testing),
PlanRecordDoneCount = planRecords.Count(x => x.Progress == PlanProgress.Done)
};
//set next reminder
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date))
{
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage))
{
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
apiResult.Add(resultToAdd);
}
return apiResult;
}
public List<ReminderRecordViewModel> GetReminders(List<Vehicle> vehicles, bool isCalendar)
{
List<ReminderRecordViewModel> reminders = new List<ReminderRecordViewModel>();
foreach (Vehicle vehicle in vehicles)
{
var vehicleReminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
if (isCalendar)
{
vehicleReminders.RemoveAll(x => x.Metric == ReminderMetric.Odometer);
//we don't care about mileages so we can basically fake the current vehicle mileage.
}
if (vehicleReminders.Any())
{
var vehicleMileage = isCalendar ? 0 : GetMaxMileage(vehicle.Id);
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, vehicleMileage, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Metric = x.Metric, Date = x.Date, Notes = x.Notes, Mileage = x.Mileage, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" }).ToList();
reminders.AddRange(reminderUrgency);
}
}
return reminders.OrderByDescending(x=>x.Urgency).ToList();
}
public List<PlanRecord> GetPlans(List<Vehicle> vehicles, bool excludeDone)
{
List<PlanRecord> plans = new List<PlanRecord>();
foreach (Vehicle vehicle in vehicles)
{
var vehiclePlans = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id);
if (excludeDone)
{
vehiclePlans.RemoveAll(x => x.Progress == PlanProgress.Done);
}
if (vehiclePlans.Any())
{
var convertedPlans = vehiclePlans.Select(x => new PlanRecord { ImportMode = x.ImportMode, Priority = x.Priority, Progress = x.Progress, Notes = x.Notes, RequisitionHistory = x.RequisitionHistory, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" });
plans.AddRange(convertedPlans);
}
}
return plans.OrderBy(x => x.Priority).ThenBy(x=>x.Progress).ToList();
}
public bool UpdateRecurringTaxes(int vehicleId)
{
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
if (!string.IsNullOrWhiteSpace(vehicleData.SoldDate))
{
return false;
}
bool RecurringTaxIsOutdated(TaxRecord taxRecord)
{
var monthInterval = taxRecord.RecurringInterval != ReminderMonthInterval.Other ? (int)taxRecord.RecurringInterval : taxRecord.CustomMonthInterval;
return DateTime.Now > taxRecord.Date.AddMonths(monthInterval);
}
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var outdatedRecurringFees = result.Where(x => x.IsRecurring && RecurringTaxIsOutdated(x));
if (outdatedRecurringFees.Any())
{
var success = false;
foreach (TaxRecord recurringFee in outdatedRecurringFees)
{
var monthInterval = recurringFee.RecurringInterval != ReminderMonthInterval.Other ? (int)recurringFee.RecurringInterval : recurringFee.CustomMonthInterval;
bool isOutdated = true;
//update the original outdated tax record
recurringFee.IsRecurring = false;
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
//month multiplier for severely outdated monthly tax records.
int monthMultiplier = 1;
var originalDate = recurringFee.Date;
while (isOutdated)
{
try
{
var nextDate = originalDate.AddMonths(monthInterval * monthMultiplier);
monthMultiplier++;
var nextnextDate = originalDate.AddMonths(monthInterval * monthMultiplier);
recurringFee.Date = nextDate;
recurringFee.Id = default; //new record
recurringFee.IsRecurring = DateTime.Now <= nextnextDate;
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
isOutdated = !recurringFee.IsRecurring;
success = true;
}
catch (Exception)
{
isOutdated = false; //break out of loop if something broke.
success = false;
}
}
}
return success;
}
return false; //no outdated recurring tax records.
}
public void RestoreSupplyRecordsByUsage(List<SupplyUsageHistory> supplyUsage, string usageDescription)
{
foreach (SupplyUsageHistory supply in supplyUsage)
{
try
{
if (supply.Id == default)
{
continue; //no id, skip current supply.
}
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.Id);
if (result != null && result.Id != default)
{
//supply exists, re-add the quantity and cost
result.Quantity += supply.Quantity;
result.Cost += supply.Cost;
var requisitionRecord = new SupplyUsageHistory
{
Id = supply.Id,
Date = DateTime.Now.Date,
Description = $"Restored from {usageDescription}",
Quantity = supply.Quantity,
Cost = supply.Cost
};
result.RequisitionHistory.Add(requisitionRecord);
//save
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(result);
}
else
{
_logger.LogError($"Unable to find supply with id {supply.Id}");
}
}
catch (Exception ex)
{
_logger.LogError($"Error restoring supply with id {supply.Id} : {ex.Message}");
}
}
}
}
}

View File

@@ -8,18 +8,21 @@ namespace CarCareTracker.MapProfile
public ImportMapper()
{
Map(m => m.Date).Name(["date", "fuelup_date"]);
Map(m => m.Day).Name(["day"]);
Map(m => m.Month).Name(["month"]);
Map(m => m.Year).Name(["year"]);
Map(m => m.DateCreated).Name(["datecreated"]);
Map(m => m.DateModified).Name(["datemodified"]);
Map(m => m.InitialOdometer).Name(["initialodometer"]);
Map(m => m.Odometer).Name(["odometer"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fuelconsumed"]);
Map(m => m.Odometer).Name(["odometer", "odo"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fuelconsumed", "qty"]);
Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]);
Map(m => m.Notes).Name("notes", "note");
Map(m => m.Price).Name(["price"]);
Map(m => m.PartialFuelUp).Name(["partial_fuelup"]);
Map(m => m.PartialFuelUp).Name(["partial_fuelup", "partial tank", "partial_fill"]);
Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]);
Map(m => m.Description).Name(["description"]);
Map(m => m.MissedFuelUp).Name(["missed_fuelup", "missedfuelup"]);
Map(m => m.MissedFuelUp).Name(["missed_fuelup", "missedfuelup", "missed fill up", "missed_fill"]);
Map(m => m.PartSupplier).Name(["partsupplier"]);
Map(m => m.PartQuantity).Name(["partquantity"]);
Map(m => m.PartNumber).Name(["partnumber"]);

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -27,7 +28,7 @@ namespace CarCareTracker.Middleware
_httpContext = httpContext;
_dataProtector = securityProvider.CreateProtector("login");
_loginLogic = loginLogic;
enableAuth = bool.Parse(configuration["EnableAuth"]);
enableAuth = bool.Parse(configuration["EnableAuth"] ?? "false");
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
@@ -42,7 +43,7 @@ namespace CarCareTracker.Middleware
new(ClaimTypes.Role, nameof(UserData.IsRootUser))
};
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
else
@@ -59,7 +60,7 @@ namespace CarCareTracker.Middleware
{
var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim();
byte[] data = Convert.FromBase64String(cleanedHeader);
string decodedString = System.Text.Encoding.UTF8.GetString(data);
string decodedString = Encoding.UTF8.GetString(data);
var splitString = decodedString.Split(":");
if (splitString.Count() != 2)
{
@@ -85,7 +86,7 @@ namespace CarCareTracker.Middleware
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
@@ -133,7 +134,7 @@ namespace CarCareTracker.Middleware
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}

138
Models/API/TypeConverter.cs Normal file
View File

@@ -0,0 +1,138 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CarCareTracker.Models
{
public class DummyType
{
}
class InvariantConverter : JsonConverter<DummyType>
{
public override void Write(Utf8JsonWriter writer, DummyType value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override DummyType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
class FromDateOptional: JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.String)
{
return reader.GetString();
}
else if (tokenType == JsonTokenType.Number)
{
if (reader.TryGetInt64(out long intInput))
{
return DateTimeOffset.FromUnixTimeSeconds(intInput).Date.ToShortDateString();
}
}
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (options.Converters.Any(x => x.Type == typeof(DummyType)))
{
writer.WriteStringValue(DateTime.Parse(value).ToString("yyyy-MM-dd"));
}
else
{
writer.WriteStringValue(value);
}
}
}
class FromDecimalOptional : JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.String)
{
return reader.GetString();
}
else if (tokenType == JsonTokenType.Number) {
if (reader.TryGetDecimal(out decimal decimalInput))
{
return decimalInput.ToString();
}
}
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (options.Converters.Any(x=>x.Type == typeof(DummyType)))
{
writer.WriteNumberValue(decimal.Parse(value));
} else
{
writer.WriteStringValue(value);
}
}
}
class FromIntOptional : JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.String)
{
return reader.GetString();
}
else if (tokenType == JsonTokenType.Number)
{
if (reader.TryGetInt32(out int intInput))
{
return intInput.ToString();
}
}
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (options.Converters.Any(x => x.Type == typeof(DummyType)))
{
writer.WriteNumberValue(int.Parse(value));
}
else
{
writer.WriteStringValue(value);
}
}
}
class FromBoolOptional : JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var tokenType = reader.TokenType;
switch (tokenType)
{
case JsonTokenType.String:
return reader.GetString();
case JsonTokenType.True:
return "True";
case JsonTokenType.False:
return "False";
default:
return reader.GetString();
}
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (options.Converters.Any(x => x.Type == typeof(DummyType)))
{
writer.WriteBooleanValue(bool.Parse(value));
}
else
{
writer.WriteStringValue(value);
}
}
}
}

27
Models/API/VehicleInfo.cs Normal file
View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class VehicleInfo
{
public Vehicle VehicleData { get; set; } = new Vehicle();
public int VeryUrgentReminderCount { get; set; }
public int UrgentReminderCount { get; set;}
public int NotUrgentReminderCount { get; set; }
public int PastDueReminderCount { get; set; }
public ReminderExportModel NextReminder { get; set; }
public int ServiceRecordCount { get; set; }
public decimal ServiceRecordCost { get; set; }
public int RepairRecordCount { get; set; }
public decimal RepairRecordCost { get; set; }
public int UpgradeRecordCount { get; set; }
public decimal UpgradeRecordCost { get; set; }
public int TaxRecordCount { get; set; }
public decimal TaxRecordCost { get; set; }
public int GasRecordCount { get; set; }
public decimal GasRecordCost { get; set; }
public int LastReportedOdometer { get; set; }
public int PlanRecordBackLogCount { get; set; }
public int PlanRecordInProgressCount { get; set; }
public int PlanRecordTestingCount { get; set; }
public int PlanRecordDoneCount { get; set; }
}
}

View File

@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public List<SupplyUsageHistory> DeletedRequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public CollisionRecord ToCollisionRecord() { return new CollisionRecord {
Id = Id,

View File

@@ -23,6 +23,7 @@
public string Notes { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public bool IncludeInAverage { get { return MilesPerGallon > 0 || (!IsFillToFull && !MissedFuelUp); } }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public bool IncludeInAverage { get { return MilesPerGallon > 0 || (!IsFillToFull && !MissedFuelUp) || (Mileage == default && !MissedFuelUp); } }
}
}

View File

@@ -0,0 +1,14 @@
namespace CarCareTracker.Models
{
public class KioskViewModel
{
/// <summary>
/// List of vehicle ids to exclude from Kiosk Dashboard
/// </summary>
public List<int> Exclusions { get; set; } = new List<int>();
/// <summary>
/// Whether to retrieve data for vehicle, plans, or reminder view.
/// </summary>
public KioskMode KioskMode { get; set; } = KioskMode.Vehicle;
}
}

View File

@@ -1,8 +1,33 @@
namespace CarCareTracker.Models
using CarCareTracker.Helper;
namespace CarCareTracker.Models
{
public class OperationResponse
public class OperationResponseBase
{
public bool Success { get; set; }
public string Message { get; set; }
}
public class OperationResponse: OperationResponseBase
{
public static OperationResponse Succeed(string message = "")
{
return new OperationResponse { Success = true, Message = message };
}
public static OperationResponse Failed(string message = "")
{
if (string.IsNullOrWhiteSpace(message))
{
message = StaticHelper.GenericErrorMessage;
}
return new OperationResponse { Success = false, Message = message};
}
public static OperationResponse Conditional(bool result, string successMessage = "", string errorMessage = "")
{
if (string.IsNullOrWhiteSpace(errorMessage))
{
errorMessage = StaticHelper.GenericErrorMessage;
}
return new OperationResponse { Success = result, Message = result ? successMessage : errorMessage };
}
}
}

View File

@@ -17,6 +17,7 @@
public decimal Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public List<SupplyUsageHistory> DeletedRequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public PlanRecord ToPlanRecord() { return new PlanRecord {
Id = Id,

View File

@@ -9,6 +9,8 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;

View File

@@ -9,6 +9,8 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
@@ -26,6 +28,8 @@
Description = Description,
Metric = Metric,
IsRecurring = IsRecurring,
UseCustomThresholds = UseCustomThresholds,
CustomThresholds = CustomThresholds,
ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval,
CustomMileageInterval = CustomMileageInterval,

View File

@@ -9,6 +9,10 @@
public string Description { get; set; }
public string Notes { get; set; }
/// <summary>
/// The metric the user selected to calculate the urgency of this reminder.
/// </summary>
public ReminderMetric UserMetric { get; set; } = ReminderMetric.Date;
/// <summary>
/// Reason why this reminder is urgent
/// </summary>
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class CostDistanceTableForVehicle
{
public string DistanceUnit { get; set; } = "mi.";
public List<CostForVehicleByMonth> CostData { get; set; } = new List<CostForVehicleByMonth>();
}
}

View File

@@ -2,9 +2,11 @@
{
public class CostForVehicleByMonth
{
public int Year { get; set; }
public int MonthId { get; set; }
public string MonthName { get; set; }
public decimal Cost { get; set; }
public int DistanceTraveled { get; set; }
public decimal CostPerDistanceTraveled { get { if (DistanceTraveled > 0) { return Cost / DistanceTraveled; } else { return 0M; } } }
}
}

View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class CostTableForVehicle
{
public string DistanceUnit { get; set; } = "Cost Per Mile";
public int TotalDistance { get; set; }
public int NumberOfDays { get; set; }
public decimal ServiceRecordSum { get; set; }
public decimal GasRecordSum { get; set; }
public decimal TaxRecordSum { get; set; }
public decimal CollisionRecordSum { get; set; }
public decimal UpgradeRecordSum { get; set; }
public decimal ServiceRecordPerMile { get { return TotalDistance != default ? ServiceRecordSum / TotalDistance : 0; } }
public decimal GasRecordPerMile { get { return TotalDistance != default ? GasRecordSum / TotalDistance : 0; } }
public decimal CollisionRecordPerMile { get { return TotalDistance != default ? CollisionRecordSum / TotalDistance : 0; } }
public decimal UpgradeRecordPerMile { get { return TotalDistance != default ? UpgradeRecordSum / TotalDistance : 0; } }
public decimal TaxRecordPerMile { get { return TotalDistance != default ? TaxRecordSum / TotalDistance : 0; } }
public decimal ServiceRecordPerDay { get { return NumberOfDays != default ? ServiceRecordSum / NumberOfDays : 0; } }
public decimal GasRecordPerDay { get { return NumberOfDays != default ? GasRecordSum / NumberOfDays : 0; } }
public decimal CollisionRecordPerDay { get { return NumberOfDays != default ? CollisionRecordSum / NumberOfDays : 0; } }
public decimal UpgradeRecordPerDay { get { return NumberOfDays != default ? UpgradeRecordSum / NumberOfDays : 0; } }
public decimal TaxRecordPerDay { get { return NumberOfDays != default ? TaxRecordSum / NumberOfDays : 0; } }
public decimal TotalPerDay { get { return ServiceRecordPerDay + CollisionRecordPerDay + UpgradeRecordPerDay + GasRecordPerDay + TaxRecordPerDay; } }
public decimal TotalPerMile { get { return ServiceRecordPerMile + CollisionRecordPerMile + UpgradeRecordPerMile + GasRecordPerMile + TaxRecordPerMile; } }
public decimal TotalCost { get { return ServiceRecordSum + CollisionRecordSum + UpgradeRecordSum + GasRecordSum + TaxRecordSum; } }
}
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class MPGForVehicleByMonth
{
public List<CostForVehicleByMonth> CostData { get; set; } = new List<CostForVehicleByMonth>();
public List<CostForVehicleByMonth> SortedCostData { get; set; } = new List<CostForVehicleByMonth>();
public string Unit { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
namespace CarCareTracker.Models
{
public class ReportParameter
{
public List<string> VisibleColumns { get; set; } = new List<string>();
public List<string> ExtraFields { get; set; } = new List<string>();
public TagFilter TagFilter { get; set; } = TagFilter.Exclude;
public List<string> Tags { get; set; } = new List<string>();
public bool FilterByDateRange { get; set; } = false;
public string StartDate { get; set; } = "";
public string EndDate { get; set; } = "";
}
}

View File

@@ -3,10 +3,11 @@
public class ReportViewModel
{
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public List<CostForVehicleByMonth> FuelMileageForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public MPGForVehicleByMonth FuelMileageForVehicleByMonth { get; set; } = new MPGForVehicleByMonth();
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
public List<int> Years { get; set; } = new List<int>();
public List<UserCollaborator> Collaborators { get; set; } = new List<UserCollaborator>();
public bool CustomWidgetsConfigured { get; set; } = false;
}
}

View File

@@ -4,6 +4,7 @@
{
public Vehicle VehicleData { get; set; }
public List<GenericReportModel> VehicleHistory { get; set; }
public ReportParameter ReportParameters { get; set; }
public string Odometer { get; set; }
public string MPG { get; set; }
public decimal TotalCost { get; set; }
@@ -16,5 +17,7 @@
public decimal TotalDepreciation { get; set; }
public decimal DepreciationPerDay { get; set; }
public decimal DepreciationPerMile { get; set; }
public string StartDate { get; set; }
public string EndDate { get; set; }
}
}

View File

@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public List<SupplyUsageHistory> DeletedRequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public ServiceRecord ToServiceRecord() { return new ServiceRecord {
Id = Id,

View File

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

View File

@@ -1,4 +1,6 @@
namespace CarCareTracker.Models
using System.Text.Json.Serialization;
namespace CarCareTracker.Models
{
/// <summary>
/// Import model used for importing records via CSV.
@@ -6,6 +8,9 @@
public class ImportModel
{
public string Date { get; set; }
public string Day { get; set; }
public string Month { get; set; }
public string Year { get; set; }
public string DateCreated { get; set; }
public string DateModified { get; set; }
public string Type { get; set; }
@@ -38,48 +43,75 @@
public string Cost { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
}
public class GenericRecordExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string Date { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class OdometerRecordExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string Date { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string InitialOdometer { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string Odometer { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class TaxRecordExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string Date { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class GasRecordExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string Date { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string Odometer { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string FuelConsumed { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string FuelEconomy { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string IsFillToFull { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string MissedFuelUp { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class ReminderExportModel
{
@@ -87,7 +119,9 @@
public string Urgency { get; set; }
public string Metric { get; set; }
public string Notes { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DueDate { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string DueOdometer { get; set; }
}
public class PlanRecordExportModel
@@ -100,6 +134,6 @@
public string Priority { get; set; }
public string Progress { get; set; }
public string Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class VehicleRecords
{
public List<ServiceRecord> ServiceRecords { get; set; } = new List<ServiceRecord>();
public List<CollisionRecord> CollisionRecords { get; set; } = new List<CollisionRecord>();
public List<UpgradeRecord> UpgradeRecords { get; set; } = new List<UpgradeRecord>();
public List<GasRecord> GasRecords { get; set; } = new List<GasRecord>();
public List<TaxRecord> TaxRecords { get; set; } = new List<TaxRecord>();
public List<OdometerRecord> OdometerRecords { get; set; } = new List<OdometerRecord>();
}
}

View File

@@ -0,0 +1,253 @@
using System.Text.Json.Serialization;
namespace CarCareTracker.Models
{
/// <summary>
/// WebHookPayload Object
/// </summary>
public class WebHookPayloadBase
{
public string Type { get; set; } = "";
public string Timestamp
{
get { return DateTime.UtcNow.ToString("O"); }
}
public Dictionary<string, string> Data { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Legacy attributes below
/// </summary>
public string VehicleId { get; set; } = "";
public string Username { get; set; } = "";
public string Action { get; set; } = "";
}
public class DiscordWebHook
{
public string Username { get { return "LubeLogger"; } }
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get { return "https://hargata.github.io/hargata/lubelogger_logo_small.png"; } }
public string Content { get; set; } = "";
public static DiscordWebHook FromWebHookPayload(WebHookPayload webHookPayload)
{
return new DiscordWebHook
{
Content = webHookPayload.Action,
};
}
}
public class WebHookPayload: WebHookPayloadBase
{
private static string GetFriendlyActionType(string actionType)
{
var actionTypeParts = actionType.Split('.');
if (actionTypeParts.Length == 2)
{
var recordType = actionTypeParts[0];
var recordAction = actionTypeParts[1];
switch (recordAction)
{
case "add":
recordAction = "Added";
break;
case "update":
recordAction = "Updated";
break;
case "delete":
recordAction = "Deleted";
break;
}
if (recordType.ToLower().Contains("record"))
{
var cleanedRecordType = recordType.ToLower().Replace("record", "");
cleanedRecordType = $"{char.ToUpper(cleanedRecordType[0])}{cleanedRecordType.Substring(1)} Record";
recordType = cleanedRecordType;
} else
{
recordType = $"{char.ToUpper(recordType[0])}{recordType.Substring(1)}";
}
return $"{recordAction} {recordType}";
} else if (actionTypeParts.Length == 3)
{
var recordType = actionTypeParts[0];
var recordAction = actionTypeParts[1];
var thirdPart = actionTypeParts[2];
switch (recordAction)
{
case "add":
recordAction = "Added";
break;
case "update":
recordAction = "Updated";
break;
case "delete":
recordAction = "Deleted";
break;
}
if (recordType.ToLower().Contains("record"))
{
var cleanedRecordType = recordType.ToLower().Replace("record", "");
cleanedRecordType = $"{char.ToUpper(cleanedRecordType[0])}{cleanedRecordType.Substring(1)} Record";
recordType = cleanedRecordType;
}
else
{
recordType = $"{char.ToUpper(recordType[0])}{recordType.Substring(1)}";
}
if (thirdPart == "api")
{
return $"{recordAction} {recordType} via API";
} else
{
return $"{recordAction} {recordType}";
}
}
return actionType;
}
public static WebHookPayload FromGenericRecord(GenericRecord genericRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", genericRecord.Description);
payloadDictionary.Add("odometer", genericRecord.Mileage.ToString());
payloadDictionary.Add("vehicleId", genericRecord.VehicleId.ToString());
payloadDictionary.Add("cost", genericRecord.Cost.ToString("F2"));
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = genericRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {genericRecord.Description}"
};
}
public static WebHookPayload FromGasRecord(GasRecord gasRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("odometer", gasRecord.Mileage.ToString());
payloadDictionary.Add("fuelconsumed", gasRecord.Gallons.ToString());
payloadDictionary.Add("vehicleId", gasRecord.VehicleId.ToString());
payloadDictionary.Add("cost", gasRecord.Cost.ToString("F2"));
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = gasRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Odometer: {gasRecord.Mileage}"
};
}
public static WebHookPayload FromOdometerRecord(OdometerRecord odometerRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("initialodometer", odometerRecord.InitialMileage.ToString());
payloadDictionary.Add("odometer", odometerRecord.Mileage.ToString());
payloadDictionary.Add("vehicleId", odometerRecord.VehicleId.ToString());
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = odometerRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Odometer: {odometerRecord.Mileage}"
};
}
public static WebHookPayload FromTaxRecord(TaxRecord taxRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", taxRecord.Description);
payloadDictionary.Add("vehicleId", taxRecord.VehicleId.ToString());
payloadDictionary.Add("cost", taxRecord.Cost.ToString("F2"));
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = taxRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {taxRecord.Description}"
};
}
public static WebHookPayload FromPlanRecord(PlanRecord planRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", planRecord.Description);
payloadDictionary.Add("vehicleId", planRecord.VehicleId.ToString());
payloadDictionary.Add("cost", planRecord.Cost.ToString("F2"));
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = planRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {planRecord.Description}"
};
}
public static WebHookPayload FromReminderRecord(ReminderRecord reminderRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", reminderRecord.Description);
payloadDictionary.Add("vehicleId", reminderRecord.VehicleId.ToString());
payloadDictionary.Add("metric", reminderRecord.Metric.ToString());
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = reminderRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {reminderRecord.Description}"
};
}
public static WebHookPayload FromSupplyRecord(SupplyRecord supplyRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", supplyRecord.Description);
payloadDictionary.Add("vehicleId", supplyRecord.VehicleId.ToString());
payloadDictionary.Add("cost", supplyRecord.Cost.ToString("F2"));
payloadDictionary.Add("quantity", supplyRecord.Quantity.ToString("F2"));
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = supplyRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {supplyRecord.Description}"
};
}
public static WebHookPayload FromNoteRecord(Note noteRecord, string actionType, string userName)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
payloadDictionary.Add("description", noteRecord.Description);
payloadDictionary.Add("vehicleId", noteRecord.VehicleId.ToString());
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = noteRecord.VehicleId.ToString(),
Username = userName,
Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {noteRecord.Description}"
};
}
public static WebHookPayload Generic(string payload, string actionType, string userName, string vehicleId)
{
Dictionary<string, string> payloadDictionary = new Dictionary<string, string>();
payloadDictionary.Add("user", userName);
if (!string.IsNullOrWhiteSpace(payload))
{
payloadDictionary.Add("description", payload);
}
return new WebHookPayload
{
Type = actionType,
Data = payloadDictionary,
VehicleId = string.IsNullOrWhiteSpace(vehicleId) ? "N/A" : vehicleId,
Username = userName,
Action = string.IsNullOrWhiteSpace(payload) ? $"{userName} {GetFriendlyActionType(actionType)}" : $"{userName} {payload}"
};
}
}
}

View File

@@ -0,0 +1,11 @@
namespace CarCareTracker.Models
{
public class SupplyAvailability
{
public bool Missing { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Required { get; set; }
public decimal InStock { get; set; }
public bool Insufficient { get { return Required > InStock; } }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class SupplyRequisitionHistory
{
public string CostInputId { get; set; }
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class SupplyStore
{
public string Tab { get; set; }
public bool AdditionalSupplies { get; set; }
}
}

View File

@@ -1,6 +1,7 @@
namespace CarCareTracker.Models
{
public class SupplyUsageHistory {
public int Id { get; set; }
public DateTime Date { get; set; }
public string PartNumber { get; set; }
public string Description { get; set; }

12
Models/Translations.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class Translations
{
public List<string> Africa { get; set; } = new List<string>();
public List<string> Asia { get; set; } = new List<string>();
public List<string> Europe { get; set; } = new List<string>();
public List<string> NorthAmerica { get; set; } = new List<string>();
public List<string> SouthAmerica { get; set; } = new List<string>();
public List<string> Oceania { get; set; } = new List<string>();
}
}

View File

@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public List<SupplyUsageHistory> DeletedRequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord {
Id = Id,

View File

@@ -3,25 +3,32 @@
public class UserConfig
{
public bool UseDarkMode { get; set; }
public bool UseSystemColorMode { get; set; }
public bool EnableCsvImports { get; set; }
public bool UseMPG { get; set; }
public bool UseDescending { get; set; }
public bool EnableAuth { get; set; }
public bool DisableRegistration { get; set; }
public bool EnableRootUserOIDC { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
public bool UseThreeDecimalGasConsumption { get; set; }
public bool UseMarkDownOnSavedNotes { get; set; }
public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; }
public bool EnableShopSupplies { get; set; }
public bool EnableExtraFieldColumns { get; set; }
public bool HideSoldVehicles { get; set; }
public bool AutomaticDecimalFormat { get; set; }
public string PreferredGasUnit { get; set; } = string.Empty;
public string PreferredGasMileageUnit { get; set; } = string.Empty;
public bool UseUnitForFuelCost { get; set; }
public List<UserColumnPreference> UserColumnPreferences { get; set; } = new List<UserColumnPreference>();
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
public string DefaultReminderEmail { get; set; } = string.Empty;
public string UserLanguage { get; set; } = "en_US";
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,
@@ -31,7 +38,21 @@
ImportMode.UpgradeRecord,
ImportMode.TaxRecord,
ImportMode.ReminderRecord,
ImportMode.NoteRecord};
ImportMode.NoteRecord
};
public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard;
public List<ImportMode> TabOrder { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,
ImportMode.PlanRecord,
ImportMode.OdometerRecord,
ImportMode.ServiceRecord,
ImportMode.RepairRecord,
ImportMode.UpgradeRecord,
ImportMode.GasRecord,
ImportMode.SupplyRecord,
ImportMode.TaxRecord,
ImportMode.NoteRecord,
ImportMode.ReminderRecord
};
}
}

View File

@@ -15,6 +15,7 @@
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public bool OdometerOptional { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
public bool HasOdometerAdjustment { get; set; } = false;
@@ -26,5 +27,10 @@
/// Primarily used for vehicles where the odometer does not reflect actual mileage.
/// </summary>
public string OdometerDifference { get; set; } = "0";
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
/// <summary>
/// Determines what is displayed in place of the license plate.
/// </summary>
public string VehicleIdentifier { get; set; } = "LicensePlate";
}
}

View File

@@ -12,9 +12,16 @@
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public bool OdometerOptional { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
public int LastReportedMileage;
public bool HasReminders = false;
public string VehicleIdentifier { get; set; } = "LicensePlate";
//Dashboard Metric Attributes
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
public int LastReportedMileage { get; set; }
public bool HasReminders { get; set; } = false;
public decimal CostPerMile { get; set; }
public decimal TotalCost { get; set; }
public string DistanceUnit { get; set; }
}
}

View File

@@ -7,11 +7,14 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.FileProviders;
var builder = WebApplication.CreateBuilder(args);
//Print Messages
StaticHelper.InitMessage(builder.Configuration);
//Check Migration
StaticHelper.CheckMigration(builder.Environment.WebRootPath, builder.Environment.ContentRootPath);
// Add services to the container.
builder.Services.AddControllersWithViews();
@@ -65,8 +68,8 @@ builder.Services.AddSingleton<IFileHelper, FileHelper>();
builder.Services.AddSingleton<IGasHelper, GasHelper>();
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<IConfigHelper, ConfigHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<ITranslationHelper, TranslationHelper>();
//configure logic
@@ -75,15 +78,6 @@ builder.Services.AddSingleton<IUserLogic, UserLogic>();
builder.Services.AddSingleton<IOdometerLogic, OdometerLogic>();
builder.Services.AddSingleton<IVehicleLogic, VehicleLogic>();
if (!Directory.Exists("data"))
{
Directory.CreateDirectory("data");
}
if (!Directory.Exists("config"))
{
Directory.CreateDirectory("config");
}
//Additional JsonFile
builder.Configuration.AddJsonFile(StaticHelper.UserConfigPath, optional: true, reloadOnChange: true);
@@ -112,11 +106,55 @@ var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "data", "images")),
RequestPath = "/images",
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/images") || ctx.Context.Request.Path.StartsWithSegments("/documents"))
if (ctx.Context.Request.Path.StartsWithSegments("/images"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
ctx.Context.Response.Redirect("/Login");
}
}
}
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "data", "documents")),
RequestPath = "/documents",
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/documents"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
ctx.Context.Response.Redirect("/Login");
}
}
}
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "data", "translations")),
RequestPath = "/translations"
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "data", "temp")),
RequestPath = "/temp",
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/temp"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)

View File

@@ -20,7 +20,7 @@ Try it out before you download it! The live demo resets every 20 minutes.
## Download
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
Read this [Getting Started Guide](https://docs.lubelogger.com/Installation/Getting%20Started) on how to download either of them
### Kubernetes Deployment
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
@@ -28,7 +28,7 @@ Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started)
### Need Help?
[Documentation](https://docs.lubelogger.com/)
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
[Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
[Search Existing Issues](https://github.com/hargata/lubelog/issues)
@@ -42,6 +42,7 @@ Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started)
- [Chart.js](https://github.com/chartjs/Chart.js)
- [Drawdown](https://github.com/adamvleggett/drawdown)
- [MailKit](https://github.com/jstedfast/MailKit)
- [Masonry](https://github.com/desandro/masonry)
## License
MIT

View File

@@ -1,5 +1,5 @@
@{
ViewData["Title"] = "LubeLogger API";
ViewData["Title"] = "API";
}
<div class="row">
<div class="d-flex justify-content-center">
@@ -26,9 +26,9 @@
<h6>Parameters</h6>
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicles</code>
@@ -40,9 +40,39 @@
No Params
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/info</code>
</div>
<div class="col-3">
Returns details for list of vehicles or specific vehicle
</div>
<div class="col-3">
VehicleId - Id of Vehicle(optional)
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/adjustedodometer</code>
</div>
<div class="col-3">
Returns odometer reading with adjustments applied
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
odometer - Unadjusted odometer
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords</code>
@@ -54,9 +84,9 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/latest</code>
@@ -68,9 +98,9 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/add</code>
@@ -86,12 +116,52 @@
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/update</code>
</div>
<div class="col-3">
Updates Odometer Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Odometer Record<br />
date - Date to be entered<br />
initialOdometer - Initial Odometer reading<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/delete</code>
</div>
<div class="col-3">
Deletes Odometer Record
</div>
<div class="col-3">
Id - Id of Odometer Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/servicerecords</code>
@@ -103,9 +173,9 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/servicerecords/add</code>
@@ -122,12 +192,53 @@
description - Description<br/>
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/servicerecords/update</code>
</div>
<div class="col-3">
Updates Service Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Service Record<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/servicerecords/delete</code>
</div>
<div class="col-3">
Deletes Service Record
</div>
<div class="col-3">
Id - Id of Service Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/repairrecords</code>
@@ -139,9 +250,9 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/repairrecords/add</code>
@@ -158,12 +269,53 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/repairrecords/update</code>
</div>
<div class="col-3">
Updates Repair Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Repair Record <br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/repairrecords/delete</code>
</div>
<div class="col-3">
Deletes Repair Record
</div>
<div class="col-3">
Id - Id of Repair Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/upgraderecords</code>
@@ -175,9 +327,9 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/upgraderecords/add</code>
@@ -194,12 +346,53 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/upgraderecords/update</code>
</div>
<div class="col-3">
Updates Upgrade Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Upgrade Record<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/upgraderecords/delete</code>
</div>
<div class="col-3">
Deletes Upgrade Record
</div>
<div class="col-3">
Id - Id of Upgrade Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/taxrecords</code>
@@ -211,9 +404,23 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/taxrecords/check</code>
</div>
<div class="col-3">
Updates Outdated Recurring Tax Records
</div>
<div class="col-3">
No Params
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/taxrecords/add</code>
@@ -229,12 +436,52 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/taxrecords/update</code>
</div>
<div class="col-3">
Updates Tax Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Tax Record<br />
date - Date to be entered<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/taxrecords/delete</code>
</div>
<div class="col-3">
Deletes Tax Record
</div>
<div class="col-3">
Id - Id of Tax Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/gasrecords</code>
@@ -250,9 +497,9 @@
useUKMPG(bool) - Use UK Imperial Calculation
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/gasrecords/add</code>
@@ -271,12 +518,55 @@
isFillToFull(bool) - Filled To Full<br />
missedFuelUp(bool) - Missed Fuel Up<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/gasrecords/update</code>
</div>
<div class="col-3">
Updates Gas Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Gas Record<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
fuelConsumed - Fuel Consumed<br />
cost - Cost<br />
isFillToFull(bool) - Filled To Full<br />
missedFuelUp(bool) - Missed Fuel Up<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/gasrecords/delete</code>
</div>
<div class="col-3">
Deletes Gas Record
</div>
<div class="col-3">
Id - Id of Gas Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders</code>
@@ -288,11 +578,27 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/documents/upload</code>
</div>
<div class="col-3">
Upload Documents
</div>
<div class="col-3">
Body(form-data): {<br />
documents[] - Files to Upload<br />
}
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/send</code>
@@ -305,9 +611,9 @@
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/makebackup</code>
@@ -319,9 +625,9 @@
No Params(must be root user)
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/cleanup</code>
@@ -339,4 +645,18 @@
$('.copyable').on('click', function (e) {
copyToClipboard(e.currentTarget);
})
function showExtraFieldsInfo(){
Swal.fire({
title: "Format for Extra Fields",
html: "extrafields[i][name] - Name of Extra Field<br/>extrafields[i][value] - Value of Extra Field<br/>i is an integer that starts at 0 and increments with each extrafield",
icon: "info"
});
}
function showAttachmentsInfo(){
Swal.fire({
title: "Attaching Files",
html: "The Document Upload Endpoint will upload the files and provide a formatted output which you can pass into this method",
icon: "info"
});
}
</script>

View File

@@ -1,13 +1,13 @@
@using CarCareTracker.Helper
@{
ViewData["Title"] = "Admin";
ViewData["Title"] = "Admin Panel";
}
@inject IConfiguration config;
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
bool emailServerIsSetup = true;
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
var userLanguage = config[nameof(UserConfig.UserLanguage)] ?? "en_US";
var mailConfig = config.GetMailConfig();
var userLanguage = config.GetServerLanguage();
if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
emailServerIsSetup = false;
@@ -25,44 +25,17 @@
</div>
<hr />
<div class="row">
<div class="col-md-5 col-12">
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
<hr />
<div class="col-12">
<div class="row">
<div class="col-6">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate User Token")</button>
<div class="col-2 d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Users")</span>
</div>
<div class="col-6 d-flex align-items-center">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Auto Notify(via Email)")</label>
</div>
<div class="col-10 d-flex align-items-center justify-content-end">
<button onclick="showTokenModal()" class="btn btn-primary btn-md mt-1 mb-1">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Manage Tokens")
</button>
</div>
</div>
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Token")</th>
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Issued To")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody>
@foreach (Token token in Model.Tokens)
{
<tr class="d-flex">
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
<td class="col-6 text-truncate">@token.EmailAddress</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-12 col-md-7">
<span class="lead">@translator.Translate(userLanguage, "Users")</span>
<hr />
<table class="table table-hover">
<thead class="sticky-top">
@@ -73,59 +46,120 @@
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody>
@foreach (UserData userData in Model.Users)
{
<tr class="d-flex" style="cursor:pointer;">
<td class="col-4">@userData.UserName</td>
<td class="col-4">@userData.EmailAddress</td>
<td class="col-2"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "")/></td>
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
</tr>
}
<tbody id="userTable">
@await Html.PartialAsync("_Users", Model.Users)
</tbody>
</table>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="tokenModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
</div>
<div class="d-flex align-items-center ms-auto">
<div class="btn-group">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate")
</button>
<button class="btn btn-outline-primary btn-md mt-1 mb-1" @(emailServerIsSetup ? "" : "disabled") onclick="toggleAutoNotify(event)">
<div class="form-check">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Notify")</label>
</div>
</button>
</div>
<button class="btn btn-close btn-md mt-1 mb-1 ms-2" onclick="hideTokenModal()" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Token")</th>
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Issued To")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody id="tokenTable">
@await Html.PartialAsync("_Tokens", Model.Tokens)
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function updateUserAdmin(userId, sender){
function showTokenModal() {
$("#tokenModal").modal('show');
}
function hideTokenModal() {
$("#tokenModal").modal('hide');
}
function reloadTokenTable() {
$.get('/Admin/GetTokenPartialView', function (data) {
$("#tokenTable").html(data);
});
}
function reloadUserTable() {
$.get('/Admin/GetUserPartialView', function (data) {
$("#userTable").html(data);
});
}
function updateUserAdmin(userId, sender) {
var isChecked = $(sender).is(":checked");
$.post('/Admin/UpdateUserAdminStatus', { userId: userId, isAdmin: isChecked }, function (data) {
if (data){
reloadPage();
if (data) {
reloadUserTable();
} else {
errorToast(genericErrorMessage());
}
});
}
function reloadPage() {
window.location.reload();
function toggleAutoNotify(e) {
if (!$("#enableAutoNotify").attr("disabled")) {
if ($(e.target).hasClass('btn')) {
$("#enableAutoNotify").trigger('click');
}
}
}
function deleteToken(tokenId) {
$.post(`/Admin/DeleteToken?tokenId=${tokenId}`, function (data) {
if (data) {
reloadPage();
reloadTokenTable();
} else {
errorToast(genericErrorMessage());
}
});
}
function deleteUser(userId) {
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
if (data) {
reloadPage();
} else {
errorToast(genericErrorMessage());
Swal.fire({
title: "Confirm Deletion?",
text: "Deleted Users cannot be restored.",
showCancelButton: true,
confirmButtonText: "Delete",
confirmButtonColor: "#dc3545"
}).then((result) => {
if (result.isConfirmed) {
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
if (data) {
reloadUserTable();
} else {
errorToast(genericErrorMessage());
}
});
}
})
});
}
function generateNewToken() {
Swal.fire({
title: 'Generate Token',
html: `
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address">
`,
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Generate',
focusConfirm: false,
preConfirm: () => {
@@ -140,7 +174,7 @@
var autoNotify = $("#enableAutoNotify").is(":checked");
$.get('/Admin/GenerateNewToken', { emailAddress: result.value.emailAddress, autoNotify: autoNotify }, function (data) {
if (data.success) {
reloadPage();
reloadTokenTable();
} else {
errorToast(data.message)
}

View File

@@ -0,0 +1,12 @@
@using CarCareTracker.Helper
@model List<Token>
@foreach (Token token in Model)
{
<tr class="d-flex">
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
<td class="col-6 text-truncate">@StaticHelper.TruncateStrings(token.EmailAddress)</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}

11
Views/Admin/_Users.cshtml Normal file
View File

@@ -0,0 +1,11 @@
@using CarCareTracker.Helper
@model List<UserData>
@foreach (UserData userData in Model)
{
<tr class="d-flex" style="cursor:pointer;">
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.UserName)</td>
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.EmailAddress)</td>
<td class="col-2 d-flex align-items-center"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "") /></td>
<td class="col-2 d-flex align-items-center"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
</tr>
}

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