Compare commits

...

188 Commits

Author SHA1 Message Date
DESKTOP-T0O5CDB\DESK-555BD
29680cf0e9 made vehicle title the edit button. 2025-05-17 10:39:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
723eb1a769 Added API endpoint to check for latest release. 2025-05-17 10:14:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
732a628c20 Merge branch 'Hargata/148.changes' into Hargata/updated.layout 2025-05-17 09:49:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84d40edb0e set graph grace see #941 2025-05-16 19:58:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9842f0e501 add report header. 2025-05-16 19:23:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
44c5f921a5 Merge branch 'Hargata/908' into Hargata/updated.layout 2025-05-16 16:54:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fae4aa31aa show vehicle thumbnail 2025-05-16 14:45:06 -06:00
Hargata Softworks
adb505c87c Merge pull request #950 from iamdabe/vehicle-home-thumbnail
Setting to show the vehicle thumbnail in the header
2025-05-16 14:31:45 -06:00
Hargata Softworks
d21b0a9e29 Merge branch 'Hargata/updated.layout' into vehicle-home-thumbnail 2025-05-16 14:31:30 -06:00
Hargata Softworks
15e4181aff Merge pull request #949 from iamdabe/main
Updated _NoteModal.cshtml label to correctly target file input 'noteFiles'
2025-05-16 14:25:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a4627ef02 use regular sized logo in mobile view in garage 2025-05-16 14:12:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d9e11273bd fix admin panel view 2025-05-16 13:49:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8e6f40a1b1 Updated layout for post hiatus release. 2025-05-16 13:44:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
55f86ecb3f add reportheader. 2025-05-16 11:11:10 -06:00
Dave Walker
8eef1465cf First commit for new setting to show the vehicle thumbnail in the header of each vehicle page. 2025-05-15 18:22:48 +02:00
iamdabe
fd0ffba7c4 Fixed _NoteModal.cshtml label to correctly target file input 'noteFiles' 2025-05-15 17:33:43 +02:00
DESKTOP-T0O5CDB\DESK-555BD
afb710e6ad Add server API version so that we can track the current version. 2025-04-30 09:51:04 -06:00
Hargata Softworks
419b755e7a Merge pull request #937 from hargata/Hargata/update.configurator.2
Add Server Domain into configurator
2025-04-27 08:58:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8cd5342c02 Add Server Domain into configurator 2025-04-27 08:57:48 -06:00
Hargata Softworks
fa32ecdb5a Merge pull request #916 from hargata/Hargata/oidc.userinfo
V1.4.7 Changes
2025-04-27 08:41:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d08726bd85 Add LUBELOGGER_DOMAIN environment variable which is used to craft registration and reset password links to reduce user friction. 2025-04-27 08:37:08 -06:00
Hargata Softworks
a87861069b Merge pull request #936 from hargata/Hargata/877
Hargata/877 - API Endpoints for Basic Reminders
2025-04-26 09:35:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b6d6a8765d update API documentation 2025-04-26 09:32:15 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f307c0933a add delete endpoint 2025-04-26 09:16:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9968ccb541 add put API 2025-04-26 09:11:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5d3746f168 add POST api endpoint for basic reminders. 2025-04-26 08:56:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
df0c4eeca2 Merge branch 'Hargata/oidc.userinfo' into Hargata/877 2025-04-26 08:15:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5b3c0aed72 Add Extra Field Type to multi edit modals 2025-04-26 07:53:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
040728b96d Fix 931 2025-04-24 20:54:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8b7b3e4ba optimized styling for planners tab 2025-04-16 11:16:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cf1f1a884b add tooltip for real long file names 2025-04-16 10:01:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c470db0590 minor UI fix on garage add. 2025-04-09 08:55:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cb71650adf API hardening, allow nulls for list types in API payload, will auto convert to empty list. 2025-04-08 08:15:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
923d59af0a update configurator 2025-04-05 08:26:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d68e1a3939 Add userinfo to retrieve email claim if not provided in id_token 2025-04-05 08:17:40 -06:00
Hargata Softworks
bf954a2946 Merge pull request #905 from hargata/Hargata/oidc.check.claims
Add debug page for advance OIDC debugging
2025-03-31 07:49:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d3f29be227 Add debug page for advance OIDC debugging 2025-03-31 07:48:29 -06:00
Hargata Softworks
5afe88a33a Merge pull request #904 from hargata/Hargata/oidc.check.claims
add openid troubleshooting mode.
2025-03-30 20:40:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6937eff576 add troubleshooting mode. 2025-03-30 20:39:25 -06:00
Hargata Softworks
6371cccc48 Merge pull request #903 from hargata/Hargata/oidc.check.claims
set default oidc claims to openid email
2025-03-30 11:30:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fc174160e0 set default oidc claims to openid email 2025-03-30 11:28:29 -06:00
Hargata Softworks
876f99fd26 Merge pull request #901 from hargata/Hargata/oidc.check.claims
Add check for email claims.
2025-03-29 07:40:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
75c65b4681 Add check for email claims. 2025-03-29 07:37:09 -06:00
Hargata Softworks
76031d27d7 Merge pull request #900 from hargata/Hargata/612
Add extra field type.
2025-03-28 10:00:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4079a93c3e Add extra field type. 2025-03-28 09:40:13 -06:00
Hargata Softworks
d913ab2009 Merge pull request #899 from hargata/Hargata/update.deps
Hargata/update.deps
2025-03-27 06:58:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6bfbcc4374 More dependencies 2025-03-27 06:57:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
73d9a7e6e9 CHORE: Update Dependencies 2025-03-27 06:50:02 -06:00
Hargata Softworks
3cbce8b584 Merge pull request #898 from hargata/Hargata/879
Allow users to hide calendar tab.
2025-03-27 06:36:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e4fcb52b24 Allow users to hide calendar tab. 2025-03-27 06:34:57 -06:00
Hargata Softworks
ae4f01ac9a Merge pull request #897 from hargata/Hargata/893
Allow users to hide Done and Delete columns.
2025-03-27 06:10:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
46b3845b3e Allow users to hide Done and Delete columns. 2025-03-27 06:06:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ffb126276f Add ID to reminderexportmodel. 2025-03-27 05:59:01 -06:00
Hargata Softworks
a9e4be823d Merge pull request #896 from hargata/Hargata/895
Hargata/895
2025-03-27 05:56:03 -06:00
DESKTOP-T0O5CDB\DESK-555BD
56cae008a6 Fixed UI and odometer adjustments invariant formats. 2025-03-27 05:54:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c2dd379ea3 Fixed vehicle API not respecting locale invariant format. 2025-03-27 05:45:48 -06:00
Hargata Softworks
baa569b323 Merge pull request #894 from hargata/Hargata/893
added urgency and due metrics for recurring reminder selector
2025-03-26 11:51:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
64ce0f8c07 added urgency and due metrics for recurring reminder selector 2025-03-26 11:33:00 -06:00
Hargata Softworks
bd98e5a6cd Merge pull request #885 from hargata/Hargata/146.changes
Display current odometer when incrementing.
2025-03-22 18:49:01 -06:00
Hargata Softworks
83fc8b8682 Merge pull request #891 from hargata/Hargata/889
remove unused aggregation.
2025-03-22 18:20:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
89fa15bbb7 remove unused aggregation. 2025-03-22 18:20:29 -06:00
Hargata Softworks
5e69be56aa Merge pull request #890 from hargata/Hargata/889
Fixed Average MPG label changing when consumption units are changed.
2025-03-22 18:11:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b1112dc617 Fixed Average MPG label changing when consumption units are changed. 2025-03-22 18:04:41 -06:00
Hargata Softworks
5a31460afe Merge pull request #886 from hargata/Hargata/server.config
Review server configs
2025-03-20 11:26:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2bcedbc7d4 translation keys 2025-03-20 11:25:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5047fdf1bc fixed button styling. 2025-03-20 11:23:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7c8c3fb1c8 added test email 2025-03-20 10:34:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9920bd472f added oidc config 2025-03-20 09:49:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d7839a8a05 Allow users to see server configuration passed in via environment variables or appsettings.json 2025-03-19 12:57:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7e07e73ef5 Display current odometer when incrementing. 2025-03-18 12:41:02 -06:00
Hargata Softworks
72a5960d40 Merge pull request #873 from hargata/Hargata/update.configurator
Fix Favicon Path
2025-02-27 12:04:18 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3429f1e4f9 Fix Favicon Path 2025-02-27 12:03:22 -07:00
Hargata Softworks
f8bea8bf81 Merge pull request #872 from hargata/Hargata/update.configurator
Update Configurator
2025-02-27 11:58:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
43794dd223 add feature to automatically merge new configurations into existing appsettings.json 2025-02-27 11:57:18 -07:00
Hargata Softworks
5cc84a7b46 Merge pull request #866 from hargata/Hargata/enter.key.qol
use generic enter key callback to handle key presses.
2025-02-19 16:09:17 -07:00
DESKTOP-T0O5CDB\DESK-555BD
48248a4386 use generic enter key callback to handle key presses. 2025-02-19 16:08:15 -07:00
Hargata Softworks
95305402e6 Merge pull request #864 from hargata/Hargata/856
add markdown to kiosk notes.
2025-02-19 09:12:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
29f24c527f add markdown to kiosk notes. 2025-02-19 09:10:03 -07:00
Hargata Softworks
efa2bbf6cc Merge pull request #858 from hargata/Hargata/857
Add flag to print individual records
2025-02-12 10:24:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
66ed9ba699 add flag to print individual records when generating vehicle history report. 2025-02-12 10:23:19 -07:00
Hargata Softworks
e841e64f78 Merge pull request #852 from hargata/Hargata/dynamic.csv
dynamically generate locale-sensitive CSV samples
2025-02-10 11:15:18 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b0f46803c5 dynamically generate locale-sensitive CSV samples 2025-02-10 11:13:53 -07:00
Hargata Softworks
ac0bef5de0 Merge pull request #850 from hargata/Hargata/840.2
more hardening.
2025-02-07 12:37:22 -07:00
DESKTOP-T0O5CDB\DESK-555BD
08ea40a08e more hardening. 2025-02-07 12:36:43 -07:00
Hargata Softworks
1d8e8059cd Merge pull request #849 from hargata/Hargata/848
fixed MPG label bugs when all tagged records are partial fuel ups.
2025-02-07 08:33:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
20b4396a4c simplified code. 2025-02-07 08:32:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4c39cb4d06 fixed MPG label bugs when all tagged records are partial fuel ups. 2025-02-07 08:29:18 -07:00
Hargata Softworks
f58e6abd9f Update issue templates 2025-02-06 22:48:32 -07:00
Hargata Softworks
4f1e83a7d7 Update issue templates 2025-02-06 22:46:55 -07:00
Hargata Softworks
e39b38c2d5 Update CONTRIBUTING.md 2025-02-06 22:35:38 -07:00
Hargata Softworks
b9ffa6ce91 Merge pull request #845 from hargata/Hargata/780
further hardening.
2025-02-05 16:28:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
76c6753785 further hardening. 2025-02-05 16:27:45 -07:00
Hargata Softworks
54f5062377 Merge pull request #844 from hargata/Hargata/780
add functionality to re-arrange table columns.
2025-02-05 16:01:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7e60fd9e40 reset var. 2025-02-05 16:00:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
926947bae4 add functionality to re-arrange table columns. 2025-02-05 15:56:20 -07:00
Hargata Softworks
2652e47018 Merge pull request #841 from hargata/Hargata/840
Hargata/840
2025-02-05 07:06:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c1500b6ed0 bump version 2025-02-04 13:22:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
125bc44d2e add put and delete api endpoints. 2025-02-04 13:15:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c317c0a058 Add POST API Endpoint to add plan records. 2025-02-04 10:07:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
beb9498399 standardize this stuff. 2025-02-03 16:50:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4e74940684 added whoami endpoint 2025-02-03 16:02:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c0f73080d2 Add APIAuth Role 2025-02-03 13:34:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cd0a35537b API endpoint for Planner 2025-02-03 13:20:08 -07:00
Hargata Softworks
4d5020b607 Merge pull request #839 from hargata/Hargata/805
re-send registration token email.
2025-02-03 08:51:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7d8b7596ce fixed wording. 2025-02-03 08:51:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
08b5c9f25a re-send registration token email. 2025-02-03 08:35:33 -07:00
Hargata Softworks
82a4f8d57b Merge pull request #833 from hargata/Hargata/831
translatable Email correspondence
2025-01-26 10:28:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
538a79319e translatable Email correspondence 2025-01-26 10:26:41 -07:00
Hargata Softworks
0555dcbc43 Merge pull request #830 from hargata/Hargata/824
take into account purchase and sold date.
2025-01-24 14:02:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7b1f19ff9f take into account purchase and sold date. 2025-01-24 14:00:26 -07:00
Hargata Softworks
9cac427eb9 Merge pull request #829 from hargata/Hargata/823
add attachments column
2025-01-24 13:13:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1e5950028f add attachments column 2025-01-24 13:01:56 -07:00
Hargata Softworks
7a7d343c3f Merge pull request #828 from hargata/Hargata/824
additional logic taking into account vehicle sold date.
2025-01-24 10:40:48 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ebf6388414 additional logic taking into account vehicle sold date. 2025-01-24 10:39:42 -07:00
Hargata Softworks
fa5426be53 Merge pull request #827 from hargata/Hargata/824
Fix number of days used when calculating cost per day.
2025-01-24 10:22:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4c60bb20c9 Fix number of days used when calculating cost per day. 2025-01-24 10:21:40 -07:00
Hargata Softworks
b02f3c8f8b Merge pull request #826 from hargata/Hargata/825
825
2025-01-24 09:17:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d5f769e5a4 Fixed the cost per distance sum table to use total cost per year divided by total distance per year. 2025-01-24 09:16:42 -07:00
Hargata Softworks
2afd1188eb Merge pull request #820 from hargata/Hargata/804
only make certain api endpoints testable
2025-01-22 12:09:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1c6301242d make urgencies endpoint parameters optional. 2025-01-22 12:07:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
06016727d9 only make certain api endpoints testable 2025-01-22 11:52:16 -07:00
Hargata Softworks
1ee0c27ff9 Merge pull request #819 from hargata/Hargata/804
make it easier to test API.
2025-01-22 11:34:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7f03a630c6 make it easier to test API. 2025-01-22 11:33:55 -07:00
Hargata Softworks
2aa19f4f3b Merge pull request #818 from hargata/Hargata/tag.bug.fix
Fix count labels for odometer and reminder records when filtered by tags
2025-01-22 09:40:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4be3c16adc Fix count labels for odometer and reminder records when filtered by tags. 2025-01-22 09:39:07 -07:00
Hargata Softworks
f1f99a67dd Merge pull request #816 from hargata/Hargata/refine.sort
only add the default sorter once.
2025-01-21 15:59:46 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a4f15ffe15 only add the default sorter once. 2025-01-21 15:57:50 -07:00
Hargata Softworks
c11dad0cb3 Merge pull request #815 from hargata/Hargata/refine.sort
get rid of storedRowTableState and all the bugs that comes with it.
2025-01-21 14:11:28 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2bf2469657 get rid of storedRowTableState and all the bugs that comes with it. 2025-01-21 14:08:44 -07:00
Hargata Softworks
a23b02e962 Merge pull request #814 from hargata/Hargata/805.2
streamline token generation process.
2025-01-20 13:14:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
905ee9bf27 streamline token generation process. 2025-01-20 13:14:04 -07:00
Hargata Softworks
e26869b30b Merge pull request #813 from hargata/Hargata/755
add days as an interval for recurring tax records and reminders
2025-01-20 12:55:56 -07:00
DESKTOP-T0O5CDB\DESK-555BD
90867792d9 missing semicolon 2025-01-20 12:51:57 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1238802a75 More Month to Time update. 2025-01-20 12:51:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8f7c50b88f Change label. 2025-01-20 12:48:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
df24434689 add days as an interval for recurring tax records and reminders 2025-01-20 12:42:59 -07:00
Hargata Softworks
878748803d Merge pull request #812 from hargata/Hargata/tech.debt
Fixed unreachable code and used Append instead of Add.
2025-01-19 12:34:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
80bbf7f22f Fixed unreachable code and used Append instead of Add. 2025-01-19 12:33:50 -07:00
Hargata Softworks
28b0ea565a Merge pull request #811 from hargata/Hargata/801.3
Add sticker printing for supplies.
2025-01-19 10:37:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
07ccd1dea9 Add sticker printing for supplies. 2025-01-19 10:36:32 -07:00
Hargata Softworks
6a8fba535a Merge pull request #810 from hargata/Hargata/801.2
added extra field and supplies to the sticker print.
2025-01-17 11:37:10 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f1b82ad0d3 removed unused row class. 2025-01-17 11:36:35 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0035bc09dd added extra field and supplies to the sticker print. 2025-01-17 11:35:41 -07:00
Hargata Softworks
863c57d7fb Merge pull request #808 from hargata/Hargata/801.2
add sticker printing for odometer and plan records.
2025-01-17 09:11:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
00ccb68ce2 add sticker printing for odometer and plan records. 2025-01-17 09:09:19 -07:00
Hargata Softworks
c2995dcd25 Merge pull request #806 from hargata/Hargata/805
enable open registration.
2025-01-16 11:23:34 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7b63366627 enable open registration. 2025-01-16 11:20:44 -07:00
Hargata Softworks
ababa6bf27 Merge pull request #803 from hargata/Hargata/801
Hargata/801 - Printable Stickers
2025-01-16 10:24:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3f1f42d4f0 add vehicle extra fields and fixed styling 2025-01-16 10:24:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4d76bd6d36 added printing functionality for notes, fuel, and tax. 2025-01-15 11:54:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ab34d1682c Added ability to print individual service, repair, and upgrade records. 2025-01-15 09:49:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
552b7a6356 simplified design. 2025-01-12 15:26:08 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f86acf673a you can now print reminders. 2025-01-12 14:47:18 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6f49bafca Added sticker viewModel and some code cleanup. 2025-01-12 12:28:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
fdbb325611 simplify code. 2025-01-12 12:04:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8c557ced85 draft PR for sticker printing. 2025-01-11 12:04:01 -07:00
Hargata Softworks
ad0f7de506 Merge pull request #802 from hargata/Hargata/697
Document the Calendar API along with support for auth.
2025-01-10 19:46:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
15ec9bb454 Document the Calendar API along with support for auth. 2025-01-10 19:43:09 -07:00
Hargata Softworks
9fe7558cfe Merge pull request #792 from hargata/Hargata/260
Simplify volume mounts.
2025-01-10 13:53:31 -07:00
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
DESKTOP-T0O5CDB\DESK-555BD
3d4b970967 Simplify volume mounts. 2025-01-08 20:56:37 -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
131 changed files with 3915 additions and 749 deletions

View File

@@ -8,6 +8,8 @@ Ideally, the Issues tab should only consist of bug reports and feature requests
## Feature Requests ## Feature Requests
Feature Requests are cool, but we do want to avoid bloat and scope/feature creep. Feature Requests are cool, but we do want to avoid bloat and scope/feature creep.
Read [this](https://github.com/hargata/lubelog/wiki/Scope-and-Purpose) to better understand the scope and purpose of this project.
LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker. LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker.
It is not and should not be used for the following: It is not and should not be used for the following:
- Project Management Software(e.g.: Jira) - Project Management Software(e.g.: Jira)

View File

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

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE REQUEST]"
labels: ''
assignees: ''
---
**Checklist**
Please make sure you have performed the following steps before submitting a new feature request, change `[ ]` to `[x]` to mark it as done
- [ ] I have read the [Contributing Guidelines](https://github.com/hargata/lubelog/blob/main/.github/CONTRIBUTING.md) and [Scope and Purpose](https://github.com/hargata/lubelog/wiki/Scope-and-Purpose)
- [ ] I have searched through existing issues.
**Description**
<!-- Describe the feature request below this line -->

11
.gitignore vendored
View File

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

View File

@@ -11,10 +11,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" /> <PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Npgsql" Version="8.0.5" /> <PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup> </ItemGroup>

View File

@@ -34,6 +34,7 @@ namespace CarCareTracker.Controllers
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
private readonly IMailHelper _mailHelper; private readonly IMailHelper _mailHelper;
private readonly IConfigHelper _config; private readonly IConfigHelper _config;
private readonly IWebHostEnvironment _webEnv;
public APIController(IVehicleDataAccess dataAccess, public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper, IGasHelper gasHelper,
IReminderHelper reminderHelper, IReminderHelper reminderHelper,
@@ -55,7 +56,8 @@ namespace CarCareTracker.Controllers
IConfigHelper config, IConfigHelper config,
IUserLogic userLogic, IUserLogic userLogic,
IVehicleLogic vehicleLogic, IVehicleLogic vehicleLogic,
IOdometerLogic odometerLogic) IOdometerLogic odometerLogic,
IWebHostEnvironment webEnv)
{ {
_dataAccess = dataAccess; _dataAccess = dataAccess;
_noteDataAccess = noteDataAccess; _noteDataAccess = noteDataAccess;
@@ -79,6 +81,7 @@ namespace CarCareTracker.Controllers
_vehicleLogic = vehicleLogic; _vehicleLogic = vehicleLogic;
_fileHelper = fileHelper; _fileHelper = fileHelper;
_config = config; _config = config;
_webEnv = webEnv;
} }
public IActionResult Index() public IActionResult Index()
{ {
@@ -89,6 +92,54 @@ namespace CarCareTracker.Controllers
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
} }
[HttpGet] [HttpGet]
[Route("/api/whoami")]
public IActionResult WhoAmI()
{
var result = new UserExportModel
{
Username = User.FindFirstValue(ClaimTypes.Name),
EmailAddress = User.IsInRole(nameof(UserData.IsRootUser)) ? _config.GetUserConfig(User).DefaultReminderEmail : User.FindFirstValue(ClaimTypes.Email),
IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)).ToString(),
IsRoot = User.IsInRole(nameof(UserData.IsRootUser)).ToString()
};
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result);
}
}
[HttpGet]
[Route("/api/version")]
public async Task<IActionResult> ServerVersion(bool checkForUpdate = false)
{
var viewModel = new ReleaseVersion
{
CurrentVersion = StaticHelper.VersionNumber,
LatestVersion = StaticHelper.VersionNumber
};
if (checkForUpdate)
{
try
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request");
var releaseResponse = await httpClient.GetFromJsonAsync<ReleaseResponse>(StaticHelper.ReleasePath) ?? new ReleaseResponse();
if (!string.IsNullOrWhiteSpace(releaseResponse.tag_name))
{
viewModel.LatestVersion = releaseResponse.tag_name;
}
}
catch (Exception ex)
{
return Json(OperationResponse.Failed($"Unable to retrieve latest version from GitHub API: {ex.Message}"));
}
}
return Json(viewModel);
}
[HttpGet]
[Route("/api/vehicles")] [Route("/api/vehicles")]
public IActionResult Vehicles() public IActionResult Vehicles()
{ {
@@ -97,8 +148,15 @@ namespace CarCareTracker.Controllers
{ {
result = _userLogic.FilterUserVehicles(result, GetUserID()); result = _userLogic.FilterUserVehicles(result, GetUserID());
} }
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result); return Json(result);
} }
}
[HttpGet] [HttpGet]
[Route("/api/vehicle/info")] [Route("/api/vehicle/info")]
@@ -150,6 +208,227 @@ namespace CarCareTracker.Controllers
return Json(convertedOdometer); return Json(convertedOdometer);
} }
} }
#region PlanRecord
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/planrecords")]
public IActionResult PlanRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = OperationResponse.Failed("Must provide a valid vehicle id");
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new PlanRecordExportModel {
Id = x.Id.ToString(),
DateCreated = x.DateCreated.ToShortDateString(),
DateModified = x.DateModified.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString(),
Notes = x.Notes,
Type = x.ImportMode.ToString(),
Priority = x.Priority.ToString(),
Progress = x.Progress.ToString(),
ExtraFields = x.ExtraFields,
Files = x.Files });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
[Consumes("application/json")]
public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input);
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
public IActionResult AddPlanRecord(int vehicleId, PlanRecordExportModel input)
{
if (vehicleId == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Must provide a valid vehicle id"));
}
if (string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Cost) ||
string.IsNullOrWhiteSpace(input.Type) ||
string.IsNullOrWhiteSpace(input.Priority) ||
string.IsNullOrWhiteSpace(input.Progress))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Description, Cost, Type, Priority, and Progress cannot be empty."));
}
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
if (!validType || !validPriority || !validProgress)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
}
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
}
if (parsedProgress == PlanProgress.Done)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
//hardening - turns null values for List types into empty lists.
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var planRecord = new PlanRecord()
{
VehicleId = vehicleId,
DateCreated = DateTime.Now,
DateModified = DateTime.Now,
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost),
ImportMode = parsedType,
Priority = parsedPriority,
Progress = parsedProgress,
ExtraFields = input.ExtraFields,
Files = input.Files
};
_planRecordDataAccess.SavePlanRecordToVehicle(planRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord, "planrecord.add.api", User.Identity.Name));
return Json(OperationResponse.Succeed("Plan Record Added"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpDelete]
[Route("/api/vehicle/planrecords/delete")]
public IActionResult DeletePlanRecord(int id)
{
var existingRecord = _planRecordDataAccess.GetPlanRecordById(id);
if (existingRecord == null || existingRecord.Id == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
//restore any requisitioned supplies.
if (existingRecord.RequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete.api", User.Identity.Name));
}
return Json(OperationResponse.Conditional(result, "Plan Record Deleted"));
}
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
[Consumes("application/json")]
public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input);
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
public IActionResult UpdatePlanRecord(PlanRecordExportModel input)
{
if (string.IsNullOrWhiteSpace(input.Id) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Cost) ||
string.IsNullOrWhiteSpace(input.Type) ||
string.IsNullOrWhiteSpace(input.Priority) ||
string.IsNullOrWhiteSpace(input.Progress))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Description, Cost, Type, Priority, and Progress cannot be empty."));
}
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
if (!validType || !validPriority || !validProgress)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
}
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
}
if (parsedProgress == PlanProgress.Done)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
var existingRecord = _planRecordDataAccess.GetPlanRecordById(int.Parse(input.Id));
if (existingRecord != null && existingRecord.Id == int.Parse(input.Id))
{
//check if user has access to the vehicleId
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
existingRecord.DateModified = DateTime.Now;
existingRecord.Description = input.Description;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ImportMode = parsedType;
existingRecord.Priority = parsedPriority;
existingRecord.Progress = parsedProgress;
existingRecord.Files = input.Files;
existingRecord.ExtraFields = input.ExtraFields;
_planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.update.api", User.Identity.Name));
}
else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
return Json(OperationResponse.Succeed("Plan Record Updated"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
#endregion
#region ServiceRecord #region ServiceRecord
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
@@ -163,7 +442,7 @@ namespace CarCareTracker.Controllers
return Json(response); return Json(response);
} }
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Tags = string.Join(' ', x.Tags) }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(result, StaticHelper.GetInvariantOption()); return Json(result, StaticHelper.GetInvariantOption());
@@ -195,6 +474,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var serviceRecord = new ServiceRecord() var serviceRecord = new ServiceRecord()
@@ -206,6 +493,7 @@ namespace CarCareTracker.Controllers
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost), Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
@@ -274,6 +562,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -291,6 +587,7 @@ namespace CarCareTracker.Controllers
existingRecord.Description = input.Description; existingRecord.Description = input.Description;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost); existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.Files = input.Files;
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord);
@@ -322,7 +619,7 @@ namespace CarCareTracker.Controllers
return Json(response); return Json(response);
} }
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId); var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Tags = string.Join(' ', x.Tags) }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(result, StaticHelper.GetInvariantOption()); return Json(result, StaticHelper.GetInvariantOption());
@@ -355,6 +652,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var repairRecord = new CollisionRecord() var repairRecord = new CollisionRecord()
@@ -366,6 +671,7 @@ namespace CarCareTracker.Controllers
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost), Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
@@ -435,6 +741,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -453,6 +767,7 @@ namespace CarCareTracker.Controllers
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost); existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Files = input.Files;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "repairrecord.update.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "repairrecord.update.api", User.Identity.Name));
@@ -484,7 +799,7 @@ namespace CarCareTracker.Controllers
return Json(response); return Json(response);
} }
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Tags = string.Join(' ', x.Tags) }); var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(result, StaticHelper.GetInvariantOption()); return Json(result, StaticHelper.GetInvariantOption());
@@ -517,6 +832,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var upgradeRecord = new UpgradeRecord() var upgradeRecord = new UpgradeRecord()
@@ -528,6 +851,7 @@ namespace CarCareTracker.Controllers
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost), Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
@@ -596,6 +920,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -614,6 +946,7 @@ namespace CarCareTracker.Controllers
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost); existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Files = input.Files;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.update.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.update.api", User.Identity.Name));
@@ -644,7 +977,7 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(response); return Json(response);
} }
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Select(x => new TaxRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Tags = string.Join(' ', x.Tags) }); var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Select(x => new TaxRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(result, StaticHelper.GetInvariantOption()); return Json(result, StaticHelper.GetInvariantOption());
@@ -711,6 +1044,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var taxRecord = new TaxRecord() var taxRecord = new TaxRecord()
@@ -721,6 +1062,7 @@ namespace CarCareTracker.Controllers
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost), Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord); _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord);
@@ -773,6 +1115,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -790,6 +1140,7 @@ namespace CarCareTracker.Controllers
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost); existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Files = input.Files;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord); _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.update.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.update.api", User.Identity.Name));
@@ -840,7 +1191,7 @@ namespace CarCareTracker.Controllers
{ {
vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords); vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords);
} }
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Tags = string.Join(' ', x.Tags) }); var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(result, StaticHelper.GetInvariantOption()); return Json(result, StaticHelper.GetInvariantOption());
@@ -871,6 +1222,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var odometerRecord = new OdometerRecord() var odometerRecord = new OdometerRecord()
@@ -881,6 +1240,7 @@ namespace CarCareTracker.Controllers
InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()) : int.Parse(input.InitialOdometer), InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()) : int.Parse(input.InitialOdometer),
Mileage = int.Parse(input.Odometer), Mileage = int.Parse(input.Odometer),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord); _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
@@ -931,6 +1291,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -948,6 +1316,7 @@ namespace CarCareTracker.Controllers
existingRecord.InitialMileage = int.Parse(input.InitialOdometer); existingRecord.InitialMileage = int.Parse(input.InitialOdometer);
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Files = input.Files;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.update.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.update.api", User.Identity.Name));
@@ -991,6 +1360,7 @@ namespace CarCareTracker.Controllers
MissedFuelUp = x.MissedFuelUp.ToString(), MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes, Notes = x.Notes,
ExtraFields = x.ExtraFields, ExtraFields = x.ExtraFields,
Files = x.Files,
Tags = string.Join(' ', x.Tags) Tags = string.Join(' ', x.Tags)
}); });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
@@ -1028,6 +1398,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
var gasRecord = new GasRecord() var gasRecord = new GasRecord()
@@ -1041,6 +1419,7 @@ namespace CarCareTracker.Controllers
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost), Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields, ExtraFields = input.ExtraFields,
Files = input.Files,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList() Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
}; };
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord); _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
@@ -1085,7 +1464,7 @@ namespace CarCareTracker.Controllers
{ {
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.delete.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.delete.api", User.Identity.Name));
} }
return Json(OperationResponse.Conditional(result, "Odometer Record Deleted")); return Json(OperationResponse.Conditional(result, "Gas Record Deleted"));
} }
[HttpPut] [HttpPut]
[Route("/api/vehicle/gasrecords/update")] [Route("/api/vehicle/gasrecords/update")]
@@ -1106,6 +1485,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400; Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
} }
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try try
{ {
//retrieve existing record //retrieve existing record
@@ -1126,6 +1513,7 @@ namespace CarCareTracker.Controllers
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost); existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ExtraFields = input.ExtraFields; existingRecord.ExtraFields = input.ExtraFields;
existingRecord.Files = input.Files;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList(); existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.update.api", User.Identity.Name)); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.update.api", User.Identity.Name));
@@ -1144,6 +1532,7 @@ namespace CarCareTracker.Controllers
} }
} }
#endregion #endregion
#region ReminderRecord
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
[Route("/api/vehicle/reminders")] [Route("/api/vehicle/reminders")]
@@ -1156,7 +1545,7 @@ namespace CarCareTracker.Controllers
} }
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId); var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString()}); var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{ {
return Json(results, StaticHelper.GetInvariantOption()); return Json(results, StaticHelper.GetInvariantOption());
@@ -1166,11 +1555,239 @@ namespace CarCareTracker.Controllers
return Json(results); return Json(results);
} }
} }
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
[Consumes("application/json")]
public IActionResult AddReminderRecordJson(int vehicleId, [FromBody] ReminderExportModel input) => AddReminderRecord(vehicleId, input);
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
public IActionResult AddReminderRecord(int vehicleId, ReminderExportModel input)
{
if (vehicleId == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Must provide a valid vehicle id"));
}
if (string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
var reminderRecord = new ReminderRecord()
{
VehicleId = vehicleId,
Description = input.Description,
Mileage = parsedOdometer,
Date = parsedDate,
Metric = parsedMetric,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
};
_reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord, "reminderrecord.add.api", User.Identity.Name));
return Json(OperationResponse.Succeed("Reminder Record Added"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpPut]
[Route("/api/vehicle/reminders/update")]
[Consumes("application/json")]
public IActionResult UpdateReminderRecordJson([FromBody] ReminderExportModel input) => UpdateReminderRecord(input);
[HttpPut]
[Route("/api/vehicle/reminders/update")]
public IActionResult UpdateReminderRecord(ReminderExportModel input)
{
if (string.IsNullOrWhiteSpace(input.Id) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
//retrieve existing record
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(int.Parse(input.Id));
if (existingRecord != null && existingRecord.Id == int.Parse(input.Id))
{
//check if user has access to the vehicleId
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
existingRecord.Date = parsedDate;
existingRecord.Mileage = parsedOdometer;
existingRecord.Description = input.Description;
existingRecord.Metric = parsedMetric;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.update.api", User.Identity.Name));
}
else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
return Json(OperationResponse.Succeed("Reminder Record Updated"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpDelete]
[Route("/api/vehicle/reminders/delete")]
public IActionResult DeleteReminderRecord(int id)
{
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(id);
if (existingRecord == null || existingRecord.Id == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete.api", User.Identity.Name));
}
return Json(OperationResponse.Conditional(result, "Reminder Record Deleted"));
}
[HttpGet]
[Route("/api/calendar")]
public IActionResult Calendar()
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
var reminders = _vehicleLogic.GetReminders(vehiclesStored, true);
var calendarContent = StaticHelper.RemindersToCalendar(reminders);
return File(calendarContent, "text/calendar");
}
#endregion
[HttpPost]
[Route("/api/documents/upload")]
public IActionResult UploadDocument(List<IFormFile> documents)
{
if (documents.Any())
{
List<UploadedFiles> uploadedFiles = new List<UploadedFiles>();
string uploadDirectory = "documents/";
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
foreach (IFormFile document in documents)
{
string fileName = Guid.NewGuid() + Path.GetExtension(document.FileName);
string filePath = Path.Combine(uploadPath, fileName);
using (var stream = System.IO.File.Create(filePath))
{
document.CopyTo(stream);
}
uploadedFiles.Add(new UploadedFiles
{
Location = Path.Combine("/", uploadDirectory, fileName),
Name = Path.GetFileName(document.FileName)
});
}
return Json(uploadedFiles);
} else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("No files to upload"));
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))] [Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet] [HttpGet]
[Route("/api/vehicle/reminders/send")] [Route("/api/vehicle/reminders/send")]
public IActionResult SendReminders(List<ReminderUrgency> urgencies) public IActionResult SendReminders(List<ReminderUrgency> urgencies)
{ {
if (!urgencies.Any())
{
//if no urgencies parameter, we will default to all urgencies.
urgencies = new List<ReminderUrgency> { ReminderUrgency.NotUrgent, ReminderUrgency.Urgent, ReminderUrgency.VeryUrgent, ReminderUrgency.PastDue };
}
var vehicles = _dataAccess.GetVehicles(); var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>(); List<OperationResponse> operationResponses = new List<OperationResponse>();
var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail; var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail;

View File

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

View File

@@ -81,7 +81,7 @@ namespace CarCareTracker.Controllers
private string UploadFile(IFormFile fileToUpload) private string UploadFile(IFormFile fileToUpload)
{ {
string uploadDirectory = "temp/"; string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory); string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
string fileName = Guid.NewGuid() + Path.GetExtension(fileToUpload.FileName); string fileName = Guid.NewGuid() + Path.GetExtension(fileToUpload.FileName);
@@ -95,7 +95,7 @@ namespace CarCareTracker.Controllers
public IActionResult UploadCoordinates(List<string> coordinates) public IActionResult UploadCoordinates(List<string> coordinates)
{ {
string uploadDirectory = "temp/"; string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory); string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
string fileName = Guid.NewGuid() + ".csv"; string fileName = Guid.NewGuid() + ".csv";

View File

@@ -23,6 +23,7 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess; private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper; private readonly IReminderHelper _reminderHelper;
private readonly ITranslationHelper _translationHelper; private readonly ITranslationHelper _translationHelper;
private readonly IMailHelper _mailHelper;
public HomeController(ILogger<HomeController> logger, public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess, IVehicleDataAccess dataAccess,
IUserLogic userLogic, IUserLogic userLogic,
@@ -33,7 +34,8 @@ namespace CarCareTracker.Controllers
IExtraFieldDataAccess extraFieldDataAccess, IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess, IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper, IReminderHelper reminderHelper,
ITranslationHelper translationHelper) ITranslationHelper translationHelper,
IMailHelper mailHelper)
{ {
_logger = logger; _logger = logger;
_dataAccess = dataAccess; _dataAccess = dataAccess;
@@ -46,6 +48,7 @@ namespace CarCareTracker.Controllers
_loginLogic = loginLogic; _loginLogic = loginLogic;
_vehicleLogic = vehicleLogic; _vehicleLogic = vehicleLogic;
_translationHelper = translationHelper; _translationHelper = translationHelper;
_mailHelper = mailHelper;
} }
private int GetUserID() private int GetUserID()
{ {
@@ -98,7 +101,6 @@ namespace CarCareTracker.Controllers
var kioskResult = _vehicleLogic.GetPlans(vehiclesStored, true); var kioskResult = _vehicleLogic.GetPlans(vehiclesStored, true);
return PartialView("_KioskPlan", kioskResult); return PartialView("_KioskPlan", kioskResult);
} }
break;
case KioskMode.Reminder: case KioskMode.Reminder:
{ {
var kioskResult = _vehicleLogic.GetReminders(vehiclesStored, false); var kioskResult = _vehicleLogic.GetReminders(vehiclesStored, false);
@@ -556,6 +558,29 @@ namespace CarCareTracker.Controllers
} }
return Json(false); return Json(false);
} }
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult GetServerConfiguration()
{
var viewModel = new ServerSettingsViewModel
{
PostgresConnection = _config.GetServerPostgresConnection(),
AllowedFileExtensions = _config.GetAllowedFileUploadExtensions(),
CustomLogoURL = _config.GetLogoUrl(),
MessageOfTheDay = _config.GetMOTD(),
WebHookURL = _config.GetWebHookUrl(),
CustomWidgetsEnabled = _config.GetCustomWidgetsEnabled(),
InvariantAPIEnabled = _config.GetInvariantApi(),
SMTPConfig = _config.GetMailConfig(),
OIDCConfig = _config.GetOpenIDConfig()
};
return PartialView("_ServerConfig", viewModel);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult SendTestEmail(string emailAddress)
{
var result = _mailHelper.SendTestEmail(emailAddress);
return Json(result);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() public IActionResult Error()
{ {

View File

@@ -49,21 +49,31 @@ namespace CarCareTracker.Controllers
} }
return View(model: redirectURL); return View(model: redirectURL);
} }
public IActionResult Registration() public IActionResult Registration(string token = "", string email = "")
{ {
if (_config.GetServerDisabledRegistration()) if (_config.GetServerDisabledRegistration())
{ {
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
return View(); var viewModel = new LoginModel
{
EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email,
Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token
};
return View(viewModel);
} }
public IActionResult ForgotPassword() public IActionResult ForgotPassword()
{ {
return View(); return View();
} }
public IActionResult ResetPassword() public IActionResult ResetPassword(string token = "", string email = "")
{ {
return View(); var viewModel = new LoginModel
{
EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email,
Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token
};
return View(viewModel);
} }
public IActionResult GetRemoteLoginLink() public IActionResult GetRemoteLoginLink()
{ {
@@ -130,13 +140,39 @@ namespace CarCareTracker.Controllers
Content = new FormUrlEncodedContent(httpParams) Content = new FormUrlEncodedContent(httpParams)
}; };
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync(); var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty; var decodedToken = JsonSerializer.Deserialize<OpenIDResult>(tokenResult);
var userJwt = decodedToken?.id_token ?? string.Empty;
var userAccessToken = decodedToken?.access_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt)) if (!string.IsNullOrWhiteSpace(userJwt))
{ {
//validate JWT token //validate JWT token
var tokenParser = new JwtSecurityTokenHandler(); var tokenParser = new JwtSecurityTokenHandler();
var parsedToken = tokenParser.ReadJwtToken(userJwt); var parsedToken = tokenParser.ReadJwtToken(userJwt);
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value; var userEmailAddress = string.Empty;
if (parsedToken.Claims.Any(x => x.Type == "email"))
{
userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
}
else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken))
{
//retrieve claims from userinfo endpoint if no email claims are returned within id_token
var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL);
userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}");
var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync();
var userInfo = JsonSerializer.Deserialize<OpenIDUserInfo>(userInfoResult);
if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty))
{
userEmailAddress = userInfo?.email ?? string.Empty;
} else
{
_logger.LogError($"OpenID Provider did not provide an email claim via UserInfo endpoint");
}
}
else
{
var returnedClaims = parsedToken.Claims.Select(x => x.Type);
_logger.LogError($"OpenID Provider did not provide an email claim, claims returned: {string.Join(",", returnedClaims)}");
}
if (!string.IsNullOrWhiteSpace(userEmailAddress)) if (!string.IsNullOrWhiteSpace(userEmailAddress))
{ {
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
@@ -180,6 +216,126 @@ namespace CarCareTracker.Controllers
} }
return new RedirectResult("/Login"); return new RedirectResult("/Login");
} }
public async Task<IActionResult> RemoteAuthDebug(string code, string state = "")
{
List<OperationResponse> results = new List<OperationResponse>();
try
{
if (!string.IsNullOrWhiteSpace(code))
{
results.Add(OperationResponse.Succeed($"Received code from OpenID Provider: {code}"));
//received code from OIDC provider
//create http client to retrieve user token from OIDC
var httpClient = new HttpClient();
var openIdConfig = _config.GetOpenIDConfig();
//check if validate state is enabled.
if (openIdConfig.ValidateState)
{
var storedStateValue = Request.Cookies["OIDC_STATE"];
if (!string.IsNullOrWhiteSpace(storedStateValue))
{
Response.Cookies.Delete("OIDC_STATE");
}
if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state)
{
results.Add(OperationResponse.Failed($"Failed State Validation - Expected: {storedStateValue} Received: {state}"));
} else
{
results.Add(OperationResponse.Succeed($"Passed State Validation - Expected: {storedStateValue} Received: {state}"));
}
}
var httpParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("client_id", openIdConfig.ClientId),
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
};
if (openIdConfig.UsePKCE)
{
//retrieve stored challenge verifier
var storedVerifier = Request.Cookies["OIDC_VERIFIER"];
if (!string.IsNullOrWhiteSpace(storedVerifier))
{
httpParams.Add(new KeyValuePair<string, string>("code_verifier", storedVerifier));
Response.Cookies.Delete("OIDC_VERIFIER");
}
}
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
{
Content = new FormUrlEncodedContent(httpParams)
};
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var decodedToken = JsonSerializer.Deserialize<OpenIDResult>(tokenResult);
var userJwt = decodedToken?.id_token ?? string.Empty;
var userAccessToken = decodedToken?.access_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt))
{
results.Add(OperationResponse.Succeed($"Passed JWT Parsing - id_token: {userJwt}"));
//validate JWT token
var tokenParser = new JwtSecurityTokenHandler();
var parsedToken = tokenParser.ReadJwtToken(userJwt);
var userEmailAddress = string.Empty;
if (parsedToken.Claims.Any(x => x.Type == "email"))
{
userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
results.Add(OperationResponse.Succeed($"Passed Claim Validation - email"));
}
else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken))
{
//retrieve claims from userinfo endpoint if no email claims are returned within id_token
var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL);
userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}");
var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync();
var userInfo = JsonSerializer.Deserialize<OpenIDUserInfo>(userInfoResult);
if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty))
{
userEmailAddress = userInfo?.email ?? string.Empty;
results.Add(OperationResponse.Succeed($"Passed Claim Validation - Retrieved email via UserInfo endpoint"));
} else
{
results.Add(OperationResponse.Failed($"Failed Claim Validation - Unable to retrieve email via UserInfo endpoint: {openIdConfig.UserInfoURL} using access_token: {userAccessToken} - Received {userInfoResult}"));
}
}
else
{
var returnedClaims = parsedToken.Claims.Select(x => x.Type);
results.Add(OperationResponse.Failed($"Failed Claim Validation - Expected: email Received: {string.Join(",", returnedClaims)}"));
}
if (!string.IsNullOrWhiteSpace(userEmailAddress))
{
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
if (userData.Id != default)
{
results.Add(OperationResponse.Succeed($"Passed User Validation - Email: {userEmailAddress} Username: {userData.UserName}"));
}
else
{
results.Add(OperationResponse.Succeed($"Passed Email Validation - Email: {userEmailAddress} User not registered"));
}
}
else
{
results.Add(OperationResponse.Failed($"Failed Email Validation - No email received from OpenID Provider"));
}
}
else
{
results.Add(OperationResponse.Failed($"Failed to parse JWT - Expected: id_token Received: {tokenResult}"));
}
}
else
{
results.Add(OperationResponse.Failed("No code received from OpenID Provider"));
}
}
catch (Exception ex)
{
results.Add(OperationResponse.Failed($"Exception: {ex.Message}"));
}
return View(results);
}
[HttpPost] [HttpPost]
public IActionResult Login(LoginModel credentials) public IActionResult Login(LoginModel credentials)
{ {
@@ -240,6 +396,12 @@ namespace CarCareTracker.Controllers
return Json(result); return Json(result);
} }
[HttpPost] [HttpPost]
public IActionResult SendRegistrationToken(LoginModel credentials)
{
var result = _loginLogic.SendRegistrationToken(credentials);
return Json(result);
}
[HttpPost]
public IActionResult RequestResetPassword(LoginModel credentials) public IActionResult RequestResetPassword(LoginModel credentials)
{ {
var result = _loginLogic.RequestResetPassword(credentials); var result = _loginLogic.RequestResetPassword(credentials);

View File

@@ -16,6 +16,176 @@ namespace CarCareTracker.Controllers
{ {
return PartialView("_BulkDataImporter", mode); return PartialView("_BulkDataImporter", mode);
} }
[HttpGet]
public IActionResult GenerateCsvSample(ImportMode mode)
{
string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
switch (mode)
{
case ImportMode.ServiceRecord:
case ImportMode.RepairRecord:
case ImportMode.UpgradeRecord:
{
var exportData = new List<GenericRecordExportModel> { new GenericRecordExportModel
{
Date = DateTime.Now.ToShortDateString(),
Description = "Test",
Cost = 123.45M.ToString("C"),
Notes = "Test Note",
Odometer = 12345.ToString(),
Tags = "test1 test2"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
case ImportMode.GasRecord:
{
var exportData = new List<GasRecordExportModel> { new GasRecordExportModel
{
Date = DateTime.Now.ToShortDateString(),
Odometer = 12345.ToString(),
FuelConsumed = 12.34M.ToString(),
Cost = 45.67M.ToString("C"),
IsFillToFull = true.ToString(),
MissedFuelUp = false.ToString(),
Notes = "Test Note",
Tags = "test1 test2"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteGasRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
case ImportMode.OdometerRecord:
{
var exportData = new List<OdometerRecordExportModel> { new OdometerRecordExportModel
{
Date = DateTime.Now.ToShortDateString(),
InitialOdometer = 12345.ToString(),
Odometer = 12345.ToString(),
Notes = "Test Note",
Tags = "test1 test2"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
case ImportMode.TaxRecord:
{
var exportData = new List<TaxRecordExportModel> { new TaxRecordExportModel
{
Date = DateTime.Now.ToShortDateString(),
Description = "Test",
Cost = 123.45M.ToString("C"),
Notes = "Test Note",
Tags = "test1 test2"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteTaxRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
case ImportMode.SupplyRecord:
{
var exportData = new List<SupplyRecordExportModel> { new SupplyRecordExportModel
{
Date = DateTime.Now.ToShortDateString(),
PartNumber = "TEST-123456",
PartSupplier = "Test Supplier",
PartQuantity = 1.5M.ToString(),
Description = "Test",
Cost = 123.45M.ToString("C"),
Notes = "Test Note",
Tags = "test1 test2"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
case ImportMode.PlanRecord:
{
var exportData = new List<PlanRecordExportModel> { new PlanRecordExportModel
{
DateCreated = DateTime.Now.ToString(),
DateModified = DateTime.Now.ToString(),
Description = "Test",
Type = ImportMode.RepairRecord.ToString(),
Priority = PlanPriority.Normal.ToString(),
Progress = PlanProgress.Testing.ToString(),
Cost = 123.45M.ToString("C"),
Notes = "Test Note"
} };
using (var writer = new StreamWriter(fullExportFilePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
//custom writer
StaticHelper.WritePlanRecordExportModel(csv, exportData);
}
writer.Dispose();
}
}
break;
default:
return Json(OperationResponse.Failed("No parameters"));
}
try
{
var fileBytes = _fileHelper.GetFileBytes(fullExportFilePath, true);
if (fileBytes.Length > 0)
{
return File(fileBytes, "text/csv", $"{mode.ToString().ToLower()}sample.csv");
}
else
{
return Json(OperationResponse.Failed("An error has occurred while generating CSV sample: file has zero bytes"));
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(OperationResponse.Failed($"An error has occurred while generating CSV sample: {ex.Message}"));
}
}
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpGet] [HttpGet]
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode) public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
@@ -25,7 +195,7 @@ namespace CarCareTracker.Controllers
return Json(false); return Json(false);
} }
string uploadDirectory = "temp/"; string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory); string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory);
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv"; var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";

View File

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

View File

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

View File

@@ -90,6 +90,7 @@ namespace CarCareTracker.Controllers
IsRecurring = result.IsRecurring, IsRecurring = result.IsRecurring,
RecurringInterval = result.RecurringInterval, RecurringInterval = result.RecurringInterval,
CustomMonthInterval = result.CustomMonthInterval, CustomMonthInterval = result.CustomMonthInterval,
CustomMonthIntervalUnit = result.CustomMonthIntervalUnit,
Files = result.Files, Files = result.Files,
Tags = result.Tags, Tags = result.Tags,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields) ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields)

View File

@@ -1,12 +1,12 @@
using CarCareTracker.External.Interfaces; using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using CarCareTracker.Helper;
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using CarCareTracker.Logic;
using CarCareTracker.Filter; using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
namespace CarCareTracker.Controllers namespace CarCareTracker.Controllers
@@ -131,7 +131,8 @@ namespace CarCareTracker.Controllers
{ {
_userLogic.AddUserAccessToVehicle(GetUserID(), vehicleInput.Id); _userLogic.AddUserAccessToVehicle(GetUserID(), vehicleInput.Id);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Created Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.add", User.Identity.Name, vehicleInput.Id.ToString())); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Created Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.add", User.Identity.Name, vehicleInput.Id.ToString()));
} else }
else
{ {
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Updated Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.update", User.Identity.Name, vehicleInput.Id.ToString())); StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Updated Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.update", User.Identity.Name, vehicleInput.Id.ToString()));
} }
@@ -215,7 +216,7 @@ namespace CarCareTracker.Controllers
{ {
return Json(searchResults); return Json(searchResults);
} }
foreach(ImportMode visibleTab in _config.GetUserConfig(User).VisibleTabs) foreach (ImportMode visibleTab in _config.GetUserConfig(User).VisibleTabs)
{ {
switch (visibleTab) switch (visibleTab)
{ {
@@ -843,14 +844,15 @@ namespace CarCareTracker.Controllers
} }
if (extraFieldIsEdited) if (extraFieldIsEdited)
{ {
foreach(ExtraField extraField in genericRecordEditModel.EditRecord.ExtraFields) foreach (ExtraField extraField in genericRecordEditModel.EditRecord.ExtraFields)
{ {
if (existingRecord.ExtraFields.Any(x=>x.Name == extraField.Name)) if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name))
{ {
var insertIndex = existingRecord.ExtraFields.FindIndex(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.RemoveAll(x => x.Name == extraField.Name);
existingRecord.ExtraFields.Insert(insertIndex, extraField); existingRecord.ExtraFields.Insert(insertIndex, extraField);
} else }
else
{ {
existingRecord.ExtraFields.Add(extraField); existingRecord.ExtraFields.Add(extraField);
} }
@@ -956,6 +958,167 @@ namespace CarCareTracker.Controllers
return Json(result); return Json(result);
} }
[HttpPost] [HttpPost]
public IActionResult PrintRecordStickers(int vehicleId, List<int> recordIds, ImportMode importMode)
{
bool result = false;
if (!recordIds.Any())
{
return Json(result);
}
var stickerViewModel = new StickerViewModel() { RecordType = importMode };
if (vehicleId != default)
{
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
if (vehicleData != null && vehicleData.Id != default)
{
stickerViewModel.VehicleData = vehicleData;
}
}
int recordsAdded = 0;
switch (importMode)
{
case ImportMode.ServiceRecord:
{
foreach (int recordId in recordIds)
{
stickerViewModel.GenericRecords.Add(_serviceRecordDataAccess.GetServiceRecordById(recordId));
recordsAdded++;
}
}
break;
case ImportMode.RepairRecord:
{
foreach (int recordId in recordIds)
{
stickerViewModel.GenericRecords.Add(_collisionRecordDataAccess.GetCollisionRecordById(recordId));
recordsAdded++;
}
}
break;
case ImportMode.UpgradeRecord:
{
foreach (int recordId in recordIds)
{
stickerViewModel.GenericRecords.Add(_upgradeRecordDataAccess.GetUpgradeRecordById(recordId));
recordsAdded++;
}
}
break;
case ImportMode.GasRecord:
{
foreach (int recordId in recordIds)
{
var record = _gasRecordDataAccess.GetGasRecordById(recordId);
stickerViewModel.GenericRecords.Add(new GenericRecord
{
Cost = record.Cost,
Date = record.Date,
Notes = record.Notes,
Mileage = record.Mileage,
ExtraFields = record.ExtraFields
});
recordsAdded++;
}
}
break;
case ImportMode.TaxRecord:
{
foreach (int recordId in recordIds)
{
var record = _taxRecordDataAccess.GetTaxRecordById(recordId);
stickerViewModel.GenericRecords.Add(new GenericRecord
{
Description = record.Description,
Cost = record.Cost,
Notes = record.Notes,
Date = record.Date,
ExtraFields = record.ExtraFields
});
recordsAdded++;
}
}
break;
case ImportMode.SupplyRecord:
{
foreach (int recordId in recordIds)
{
var record = _supplyRecordDataAccess.GetSupplyRecordById(recordId);
stickerViewModel.SupplyRecords.Add(record);
recordsAdded++;
}
}
break;
case ImportMode.NoteRecord:
{
foreach (int recordId in recordIds)
{
var record = _noteDataAccess.GetNoteById(recordId);
stickerViewModel.GenericRecords.Add(new GenericRecord
{
Description = record.Description,
Notes = record.NoteText
});
recordsAdded++;
}
}
break;
case ImportMode.OdometerRecord:
{
foreach (int recordId in recordIds)
{
var record = _odometerRecordDataAccess.GetOdometerRecordById(recordId);
stickerViewModel.GenericRecords.Add(new GenericRecord
{
Date = record.Date,
Mileage = record.Mileage,
Notes = record.Notes,
ExtraFields = record.ExtraFields
});
recordsAdded++;
}
}
break;
case ImportMode.ReminderRecord:
{
foreach (int recordId in recordIds)
{
stickerViewModel.ReminderRecords.Add(_reminderRecordDataAccess.GetReminderRecordById(recordId));
recordsAdded++;
}
}
break;
case ImportMode.PlanRecord:
{
foreach (int recordId in recordIds)
{
var record = _planRecordDataAccess.GetPlanRecordById(recordId);
stickerViewModel.GenericRecords.Add(new GenericRecord
{
Description = record.Description,
Cost = record.Cost,
Notes = record.Notes,
Date = record.DateModified,
ExtraFields = record.ExtraFields,
RequisitionHistory = record.RequisitionHistory
});
recordsAdded++;
}
}
break;
}
if (recordsAdded > 0)
{
return PartialView("_Stickers", stickerViewModel);
}
return Json(result);
}
[HttpPost]
public IActionResult SaveUserColumnPreferences(UserColumnPreference columnPreference) public IActionResult SaveUserColumnPreferences(UserColumnPreference columnPreference)
{ {
try try
@@ -966,6 +1129,7 @@ namespace CarCareTracker.Controllers
{ {
var existingPreference = existingUserColumnPreference.Single(); var existingPreference = existingUserColumnPreference.Single();
existingPreference.VisibleColumns = columnPreference.VisibleColumns; existingPreference.VisibleColumns = columnPreference.VisibleColumns;
existingPreference.ColumnOrder = columnPreference.ColumnOrder;
} }
else else
{ {

12
Enum/ExtraFieldType.cs Normal file
View File

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

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public enum ReminderIntervalUnit
{
Months = 1,
Days = 2
}
}

View File

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

View File

@@ -6,6 +6,7 @@ namespace CarCareTracker.Helper
public interface IFileHelper public interface IFileHelper
{ {
string GetFullFilePath(string currentFilePath, bool mustExist = true); string GetFullFilePath(string currentFilePath, bool mustExist = true);
byte[] GetFileBytes(string fullFilePath, bool deleteFile = false);
string MoveFileFromTemp(string currentFilePath, string newFolder); string MoveFileFromTemp(string currentFilePath, string newFolder);
bool RenameFile(string currentFilePath, string newName); bool RenameFile(string currentFilePath, string newName);
bool DeleteFile(string currentFilePath); bool DeleteFile(string currentFilePath);
@@ -34,7 +35,7 @@ namespace CarCareTracker.Helper
} }
public List<string> GetLanguages() 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" }; var defaultList = new List<string>() { "en_US" };
if (Directory.Exists(languagePath)) if (Directory.Exists(languagePath))
{ {
@@ -72,7 +73,7 @@ namespace CarCareTracker.Helper
{ {
currentFilePath = currentFilePath.Substring(1); 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)) if (File.Exists(oldFilePath))
{ {
return oldFilePath; return oldFilePath;
@@ -85,6 +86,19 @@ namespace CarCareTracker.Helper
return string.Empty; return string.Empty;
} }
} }
public byte[] GetFileBytes(string fullFilePath, bool deleteFile = false)
{
if (File.Exists(fullFilePath))
{
var fileBytes = File.ReadAllBytes(fullFilePath);
if (deleteFile)
{
File.Delete(fullFilePath);
}
return fileBytes;
}
return Array.Empty<byte>();
}
public bool RestoreBackup(string fileName, bool clearExisting = false) public bool RestoreBackup(string fileName, bool clearExisting = false)
{ {
var fullFilePath = GetFullFilePath(fileName); var fullFilePath = GetFullFilePath(fileName);
@@ -94,7 +108,7 @@ namespace CarCareTracker.Helper
} }
try try
{ {
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{Guid.NewGuid()}"); var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{Guid.NewGuid()}");
if (!Directory.Exists(tempPath)) if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath); Directory.CreateDirectory(tempPath);
//extract zip file //extract zip file
@@ -105,10 +119,10 @@ namespace CarCareTracker.Helper
var translationPath = Path.Combine(tempPath, "translations"); var translationPath = Path.Combine(tempPath, "translations");
var dataPath = Path.Combine(tempPath, StaticHelper.DbName); var dataPath = Path.Combine(tempPath, StaticHelper.DbName);
var widgetPath = Path.Combine(tempPath, StaticHelper.AdditionalWidgetsPath); var widgetPath = Path.Combine(tempPath, StaticHelper.AdditionalWidgetsPath);
var configPath = Path.Combine(tempPath, StaticHelper.UserConfigPath); var configPath = Path.Combine(tempPath, StaticHelper.LegacyUserConfigPath);
if (Directory.Exists(imagePath)) if (Directory.Exists(imagePath))
{ {
var existingPath = Path.Combine(_webEnv.WebRootPath, "images"); var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "images");
if (!Directory.Exists(existingPath)) if (!Directory.Exists(existingPath))
{ {
Directory.CreateDirectory(existingPath); Directory.CreateDirectory(existingPath);
@@ -130,7 +144,7 @@ namespace CarCareTracker.Helper
} }
if (Directory.Exists(documentPath)) if (Directory.Exists(documentPath))
{ {
var existingPath = Path.Combine(_webEnv.WebRootPath, "documents"); var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents");
if (!Directory.Exists(existingPath)) if (!Directory.Exists(existingPath))
{ {
Directory.CreateDirectory(existingPath); Directory.CreateDirectory(existingPath);
@@ -152,7 +166,7 @@ namespace CarCareTracker.Helper
} }
if (Directory.Exists(translationPath)) if (Directory.Exists(translationPath))
{ {
var existingPath = Path.Combine(_webEnv.WebRootPath, "translations"); var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations");
if (!Directory.Exists(existingPath)) if (!Directory.Exists(existingPath))
{ {
Directory.CreateDirectory(existingPath); Directory.CreateDirectory(existingPath);
@@ -186,9 +200,9 @@ namespace CarCareTracker.Helper
if (File.Exists(configPath)) if (File.Exists(configPath))
{ {
//check if config folder exists. //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); File.Move(configPath, StaticHelper.UserConfigPath, true);
} }
@@ -203,7 +217,7 @@ namespace CarCareTracker.Helper
public string MakeAttachmentsExport(List<GenericReportModel> exportData) public string MakeAttachmentsExport(List<GenericReportModel> exportData)
{ {
var folderName = Guid.NewGuid(); 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)) if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath); Directory.CreateDirectory(tempPath);
int fileIndex = 0; int fileIndex = 0;
@@ -227,10 +241,10 @@ namespace CarCareTracker.Helper
public string MakeBackup() public string MakeBackup()
{ {
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}"; var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}"); var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{folderName}");
var imagePath = Path.Combine(_webEnv.WebRootPath, "images"); var imagePath = Path.Combine(_webEnv.ContentRootPath, "data", "images");
var documentPath = Path.Combine(_webEnv.WebRootPath, "documents"); var documentPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents");
var translationPath = Path.Combine(_webEnv.WebRootPath, "translations"); var translationPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations");
var dataPath = StaticHelper.DbName; var dataPath = StaticHelper.DbName;
var widgetPath = StaticHelper.AdditionalWidgetsPath; var widgetPath = StaticHelper.AdditionalWidgetsPath;
var configPath = StaticHelper.UserConfigPath; var configPath = StaticHelper.UserConfigPath;
@@ -301,8 +315,8 @@ namespace CarCareTracker.Helper
{ {
currentFilePath = currentFilePath.Substring(1); currentFilePath = currentFilePath.Substring(1);
} }
string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder); string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", newFolder);
string oldFilePath = Path.Combine(_webEnv.WebRootPath, currentFilePath); string oldFilePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath);
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
string newFileUploadPath = oldFilePath.Replace(tempPath, newFolder); string newFileUploadPath = oldFilePath.Replace(tempPath, newFolder);
@@ -319,7 +333,7 @@ namespace CarCareTracker.Helper
{ {
currentFilePath = currentFilePath.Substring(1); currentFilePath = currentFilePath.Substring(1);
} }
string filePath = Path.Combine(_webEnv.WebRootPath, currentFilePath); string filePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath);
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
File.Delete(filePath); File.Delete(filePath);

View File

@@ -76,7 +76,8 @@ namespace CarCareTracker.Helper
MissedFuelUp = currentObject.MissedFuelUp, MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes, Notes = currentObject.Notes,
Tags = currentObject.Tags, Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields ExtraFields = currentObject.ExtraFields,
Files = currentObject.Files
}; };
if (currentObject.MissedFuelUp) if (currentObject.MissedFuelUp)
{ {
@@ -86,9 +87,9 @@ namespace CarCareTracker.Helper
unFactoredConsumption = 0; unFactoredConsumption = 0;
unFactoredMileage = 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) if (convertedConsumption > 0.00M && deltaMileage > 0)
{ {
try try
@@ -130,11 +131,15 @@ namespace CarCareTracker.Helper
MissedFuelUp = currentObject.MissedFuelUp, MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes, Notes = currentObject.Notes,
Tags = currentObject.Tags, Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields ExtraFields = currentObject.ExtraFields,
Files = currentObject.Files
}); });
} }
if (currentObject.Mileage != default)
{
previousMileage = currentObject.Mileage; previousMileage = currentObject.Mileage;
} }
}
return computedResults; return computedResults;
} }
} }

View File

@@ -11,20 +11,28 @@ namespace CarCareTracker.Helper
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token); OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token); OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders); OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
OperationResponse SendTestEmail(string emailAddress);
} }
public class MailHelper : IMailHelper public class MailHelper : IMailHelper
{ {
private readonly MailConfig mailConfig; private readonly MailConfig mailConfig;
private readonly string serverLanguage;
private readonly string serverDomain;
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
private readonly ITranslationHelper _translator;
private readonly ILogger<MailHelper> _logger; private readonly ILogger<MailHelper> _logger;
public MailHelper( public MailHelper(
IConfigHelper config, IConfigHelper config,
IFileHelper fileHelper, IFileHelper fileHelper,
ITranslationHelper translationHelper,
ILogger<MailHelper> logger ILogger<MailHelper> logger
) { ) {
//load mailConfig from Configuration //load mailConfig from Configuration
mailConfig = config.GetMailConfig(); mailConfig = config.GetMailConfig();
serverLanguage = config.GetServerLanguage();
serverDomain = config.GetServerDomain();
_fileHelper = fileHelper; _fileHelper = fileHelper;
_translator = translationHelper;
_logger = logger; _logger = logger;
} }
public OperationResponse NotifyUserForRegistration(string emailAddress, string token) public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
@@ -36,8 +44,15 @@ namespace CarCareTracker.Helper
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) { if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) {
return OperationResponse.Failed("Email Address or Token is invalid"); return OperationResponse.Failed("Email Address or Token is invalid");
} }
string emailSubject = "Your Registration Token for LubeLogger"; string emailSubject = _translator.Translate(serverLanguage, "Your Registration Token for LubeLogger");
string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}"; string tokenHtml = token;
if (!string.IsNullOrWhiteSpace(serverDomain))
{
string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain;
//construct registration URL.
tokenHtml = $"<a href='{cleanedURL}/Login/Registration?email={emailAddress}&token={token}' target='_blank'>{token}</a>";
}
string emailBody = $"<span>{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please complete your registration for LubeLogger using the token")}: {tokenHtml}</span>";
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {
@@ -57,8 +72,37 @@ namespace CarCareTracker.Helper
{ {
return OperationResponse.Failed("Email Address or Token is invalid"); return OperationResponse.Failed("Email Address or Token is invalid");
} }
string emailSubject = "Your Password Reset Token for LubeLogger"; string emailSubject = _translator.Translate(serverLanguage, "Your Password Reset Token for LubeLogger");
string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}"; string tokenHtml = token;
if (!string.IsNullOrWhiteSpace(serverDomain))
{
string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain;
//construct registration URL.
tokenHtml = $"<a href='{cleanedURL}/Login/ResetPassword?email={emailAddress}&token={token}' target='_blank'>{token}</a>";
}
string emailBody = $"<span>{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please reset your password for LubeLogger using the token")}: {tokenHtml}</span>";
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return OperationResponse.Succeed("Email Sent!");
}
else
{
return OperationResponse.Failed();
}
}
public OperationResponse SendTestEmail(string emailAddress)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return OperationResponse.Failed("SMTP Server Not Setup");
}
if (string.IsNullOrWhiteSpace(emailAddress))
{
return OperationResponse.Failed("Email Address or Token is invalid");
}
string emailSubject = _translator.Translate(serverLanguage, "Test Email from LubeLogger");
string emailBody = _translator.Translate(serverLanguage, "If you are seeing this email it means your SMTP configuration is functioning correctly");
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody); var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result) if (result)
{ {
@@ -79,8 +123,8 @@ namespace CarCareTracker.Helper
{ {
return OperationResponse.Failed("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 emailSubject = _translator.Translate(serverLanguage, "Your User Account Update Token for LubeLogger");
string emailBody = $"A token has been generated on your behalf, please update your account for LubeLogger using the token: {token}"; string emailBody = $"{_translator.Translate(serverLanguage, "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); var result = SendEmail(new List<string> { emailAddress}, emailSubject, emailBody);
if (result) if (result)
{ {
@@ -107,17 +151,18 @@ namespace CarCareTracker.Helper
} }
//get email template, this file has to exist since it's a static file. //get email template, this file has to exist since it's a static file.
var emailTemplatePath = _fileHelper.GetFullFilePath(StaticHelper.ReminderEmailTemplate); var emailTemplatePath = _fileHelper.GetFullFilePath(StaticHelper.ReminderEmailTemplate);
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}"; string emailSubject = $"{_translator.Translate(serverLanguage, "Vehicle Reminders From LubeLogger")} - {DateTime.Now.ToShortDateString()}";
//construct html table. //construct html table.
string emailBody = File.ReadAllText(emailTemplatePath); string emailBody = File.ReadAllText(emailTemplatePath);
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}"); emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}");
string tableHeader = $"<th>{_translator.Translate(serverLanguage, "Urgency")}</th><th>{_translator.Translate(serverLanguage, "Description")}</th><th>{_translator.Translate(serverLanguage, "Due")}</th>";
string tableBody = ""; string tableBody = "";
foreach(ReminderRecordViewModel reminder in reminders) foreach(ReminderRecordViewModel reminder in reminders)
{ {
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} 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>"; tableBody += $"<tr class='{reminder.Urgency}'><td>{_translator.Translate(serverLanguage, StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency))}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
} }
emailBody = emailBody.Replace("{TableBody}", tableBody); emailBody = emailBody.Replace("{TableHeader}", tableHeader).Replace("{TableBody}", tableBody);
try try
{ {
var result = SendEmail(emailAddresses, emailSubject, emailBody); var result = SendEmail(emailAddresses, emailSubject, emailBody);

View File

@@ -24,9 +24,16 @@ namespace CarCareTracker.Helper
{ {
existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval); existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval);
} else } else
{
if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months)
{ {
existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval); existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval);
} }
else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days)
{
existingReminder.Date = newDate.Date.AddDays(existingReminder.CustomMonthInterval);
}
}
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other) if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
{ {
@@ -54,9 +61,16 @@ namespace CarCareTracker.Helper
existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval); existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval);
} }
else else
{
if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months)
{ {
existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval); existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval);
} }
else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days)
{
existingReminder.Date = newDate.AddDays(existingReminder.CustomMonthInterval);
}
}
} }
return existingReminder; return existingReminder;
} }

View File

@@ -1,6 +1,8 @@
using CarCareTracker.Models; using CarCareTracker.Models;
using CsvHelper; using CsvHelper;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace CarCareTracker.Helper namespace CarCareTracker.Helper
@@ -10,15 +12,17 @@ namespace CarCareTracker.Helper
/// </summary> /// </summary>
public static class StaticHelper public static class StaticHelper
{ {
public const string VersionNumber = "1.4.2"; public const string VersionNumber = "1.4.8";
public const string DbName = "data/cartracker.db"; public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "config/userConfig.json"; 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 AdditionalWidgetsPath = "data/widgets.html";
public const string GenericErrorMessage = "An error occurred, please try again later"; public const string GenericErrorMessage = "An error occurred, please try again later";
public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt"; public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx"; public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json"; public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public const string TranslationPath = "https://hargata.github.io/lubelog_translations"; public const string TranslationPath = "https://hargata.github.io/lubelog_translations";
public const string ReleasePath = "https://api.github.com/repos/hargata/lubelog/releases/latest";
public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json"; public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com"; public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input) public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
@@ -240,7 +244,8 @@ namespace CarCareTracker.Helper
public static List<ExtraField> AddExtraFields(List<ExtraField> recordExtraFields, List<ExtraField> templateExtraFields) public static List<ExtraField> AddExtraFields(List<ExtraField> recordExtraFields, List<ExtraField> templateExtraFields)
{ {
if (!templateExtraFields.Any()) { if (!templateExtraFields.Any())
{
return new List<ExtraField>(); return new List<ExtraField>();
} }
if (!recordExtraFields.Any()) if (!recordExtraFields.Any())
@@ -258,10 +263,12 @@ namespace CarCareTracker.Helper
//update isrequired setting //update isrequired setting
foreach (ExtraField extraField in recordExtraFields) foreach (ExtraField extraField in recordExtraFields)
{ {
extraField.IsRequired = templateExtraFields.Where(x => x.Name == extraField.Name).First().IsRequired; var firstMatchingField = templateExtraFields.First(x => x.Name == extraField.Name);
extraField.IsRequired = firstMatchingField.IsRequired;
extraField.FieldType = firstMatchingField.FieldType;
} }
//append extra fields //append extra fields
foreach(ExtraField extraField in templateExtraFields) foreach (ExtraField extraField in templateExtraFields)
{ {
if (!recordFieldNames.Contains(extraField.Name)) if (!recordFieldNames.Contains(extraField.Name))
{ {
@@ -309,7 +316,8 @@ namespace CarCareTracker.Helper
if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer)) if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{ {
Console.WriteLine($"SMTP Configured for {mailConfig.EmailServer}"); Console.WriteLine($"SMTP Configured for {mailConfig.EmailServer}");
} else }
else
{ {
Console.WriteLine("SMTP Not Configured"); Console.WriteLine("SMTP Not Configured");
} }
@@ -317,7 +325,94 @@ namespace CarCareTracker.Helper
Console.WriteLine($"Message Of The Day: {motd}"); Console.WriteLine($"Message Of The Day: {motd}");
if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name)) if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name))
{ {
Console.WriteLine("No Locale or Culture Configured for LubeLogger, Check Environment Variables"); 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 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) public static async void NotifyAsync(string webhookURL, WebHookPayload webHookPayload)
@@ -332,7 +427,8 @@ namespace CarCareTracker.Helper
webhookURL = webhookURL.Replace("discord://", "https://"); //cleanurl webhookURL = webhookURL.Replace("discord://", "https://"); //cleanurl
//format to discord //format to discord
httpClient.PostAsJsonAsync(webhookURL, DiscordWebHook.FromWebHookPayload(webHookPayload)); httpClient.PostAsJsonAsync(webhookURL, DiscordWebHook.FromWebHookPayload(webHookPayload));
} else }
else
{ {
httpClient.PostAsJsonAsync(webhookURL, webHookPayload); httpClient.PostAsJsonAsync(webhookURL, webHookPayload);
} }
@@ -370,12 +466,14 @@ namespace CarCareTracker.Helper
if (vehicle.VehicleIdentifier == "LicensePlate") if (vehicle.VehicleIdentifier == "LicensePlate")
{ {
return vehicle.LicensePlate; return vehicle.LicensePlate;
} else }
else
{ {
if (vehicle.ExtraFields.Any(x=>x.Name == vehicle.VehicleIdentifier)) if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier))
{ {
return vehicle.ExtraFields?.FirstOrDefault(x=>x.Name == vehicle.VehicleIdentifier)?.Value; return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value;
} else }
else
{ {
return "N/A"; return "N/A";
} }
@@ -402,7 +500,8 @@ namespace CarCareTracker.Helper
//Translations //Translations
public static string GetTranslationDownloadPath(string continent, string name) public static string GetTranslationDownloadPath(string continent, string name)
{ {
if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name)){ if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name))
{
return string.Empty; return string.Empty;
} }
else else
@@ -424,7 +523,8 @@ namespace CarCareTracker.Helper
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
{ {
return string.Empty; return string.Empty;
} else }
else
{ {
try try
{ {
@@ -438,7 +538,8 @@ namespace CarCareTracker.Helper
{ {
return displayName; return displayName;
} }
} catch (Exception ex) }
catch (Exception ex)
{ {
return name; return name;
} }
@@ -609,7 +710,8 @@ namespace CarCareTracker.Helper
if (input == 0M.ToString("C2") && hideZero) if (input == 0M.ToString("C2") && hideZero)
{ {
return "---"; return "---";
} else }
else
{ {
return string.IsNullOrWhiteSpace(decorations) ? input : $"{input}{decorations}"; return string.IsNullOrWhiteSpace(decorations) ? input : $"{input}{decorations}";
} }
@@ -669,5 +771,52 @@ namespace CarCareTracker.Helper
_csv.NextRecord(); _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

@@ -21,6 +21,7 @@ namespace CarCareTracker.Logic
OperationResponse RequestResetPassword(LoginModel credentials); OperationResponse RequestResetPassword(LoginModel credentials);
OperationResponse ResetPasswordByUser(LoginModel credentials); OperationResponse ResetPasswordByUser(LoginModel credentials);
OperationResponse ResetUserPassword(LoginModel credentials); OperationResponse ResetUserPassword(LoginModel credentials);
OperationResponse SendRegistrationToken(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials); UserData ValidateUserCredentials(LoginModel credentials);
UserData ValidateOpenIDUser(LoginModel credentials); UserData ValidateOpenIDUser(LoginModel credentials);
bool CheckIfUserIsValid(int userId); bool CheckIfUserIsValid(int userId);
@@ -190,6 +191,16 @@ namespace CarCareTracker.Logic
return OperationResponse.Failed(); return OperationResponse.Failed();
} }
} }
public OperationResponse SendRegistrationToken(LoginModel credentials)
{
if (_configHelper.GetServerOpenRegistration())
{
return GenerateUserToken(credentials.EmailAddress, true);
} else
{
return OperationResponse.Failed("Open Registration Disabled");
}
}
/// <summary> /// <summary>
/// Generates a token and notifies user via email so they can reset their password. /// Generates a token and notifies user via email so they can reset their password.
/// </summary> /// </summary>
@@ -310,7 +321,18 @@ namespace CarCareTracker.Logic
var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress); var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress);
if (existingToken.Id != default) if (existingToken.Id != default)
{ {
return OperationResponse.Failed("There is an existing token tied to this email address"); if (autoNotify) //re-send email
{
var notificationResult = _mailHelper.NotifyUserForRegistration(emailAddress, existingToken.Body);
if (notificationResult.Success)
{
return OperationResponse.Failed($"There is an existing token tied to {emailAddress}, a new email has been sent out");
} else
{
return notificationResult;
}
}
return OperationResponse.Failed($"There is an existing token tied to {emailAddress}");
} }
var token = new Token() var token = new Token()
{ {

View File

@@ -13,7 +13,7 @@ namespace CarCareTracker.Logic
int GetMaxMileage(VehicleRecords vehicleRecords); int GetMaxMileage(VehicleRecords vehicleRecords);
int GetMinMileage(int vehicleId); int GetMinMileage(int vehicleId);
int GetMinMileage(VehicleRecords vehicleRecords); 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); int GetOwnershipDays(string purchaseDate, string soldDate, int year, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage); bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage);
List<VehicleInfo> GetVehicleInfo(List<Vehicle> vehicles); List<VehicleInfo> GetVehicleInfo(List<Vehicle> vehicles);
List<ReminderRecordViewModel> GetReminders(List<Vehicle> vehicles, bool isCalendar); List<ReminderRecordViewModel> GetReminders(List<Vehicle> vehicles, bool isCalendar);
@@ -199,22 +199,44 @@ namespace CarCareTracker.Logic
} }
return numbersArray.Any() ? numbersArray.Min() : 0; 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) public int GetOwnershipDays(string purchaseDate, string soldDate, int year, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords)
{ {
var startDate = DateTime.Now; var startDate = DateTime.Now;
var endDate = DateTime.Now; var endDate = DateTime.Now;
if (!string.IsNullOrWhiteSpace(soldDate)) bool usePurchaseDate = false;
bool useSoldDate = false;
if (!string.IsNullOrWhiteSpace(soldDate) && DateTime.TryParse(soldDate, out DateTime vehicleSoldDate))
{ {
endDate = DateTime.Parse(soldDate); if (year == default || year >= vehicleSoldDate.Year) //All Time is selected or the selected year is greater or equal to the year the vehicle is sold
{
endDate = vehicleSoldDate; //cap end date to vehicle sold date.
useSoldDate = true;
} }
if (!string.IsNullOrWhiteSpace(purchaseDate)) }
if (!string.IsNullOrWhiteSpace(purchaseDate) && DateTime.TryParse(purchaseDate, out DateTime vehiclePurchaseDate))
{ {
//if purchase date is provided, then we just have to subtract the begin date to end date and return number of months if (year == default || year <= vehiclePurchaseDate.Year) //All Time is selected or the selected year is less or equal to the year the vehicle is purchased
startDate = DateTime.Parse(purchaseDate); {
startDate = vehiclePurchaseDate; //cap start date to vehicle purchase date
usePurchaseDate = true;
}
}
if (year != default)
{
var calendarYearStart = new DateTime(year, 1, 1);
var calendarYearEnd = new DateTime(year + 1, 1, 1);
if (!useSoldDate)
{
endDate = endDate > calendarYearEnd ? calendarYearEnd : endDate;
}
if (!usePurchaseDate)
{
startDate = startDate > calendarYearStart ? calendarYearStart : startDate;
}
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays); var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed; return timeElapsed;
} }
var dateArray = new List<DateTime>(); var dateArray = new List<DateTime>() { startDate };
dateArray.AddRange(serviceRecords.Select(x => x.Date)); dateArray.AddRange(serviceRecords.Select(x => x.Date));
dateArray.AddRange(repairRecords.Select(x => x.Date)); dateArray.AddRange(repairRecords.Select(x => x.Date));
dateArray.AddRange(gasRecords.Select(x => x.Date)); dateArray.AddRange(gasRecords.Select(x => x.Date));
@@ -281,11 +303,11 @@ namespace CarCareTracker.Logic
//set next reminder //set next reminder
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date)) if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date))
{ {
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First(); resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) }).First();
} }
else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage)) else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage))
{ {
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First(); resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) }).First();
} }
apiResult.Add(resultToAdd); apiResult.Add(resultToAdd);
} }
@@ -340,7 +362,8 @@ namespace CarCareTracker.Logic
bool RecurringTaxIsOutdated(TaxRecord taxRecord) bool RecurringTaxIsOutdated(TaxRecord taxRecord)
{ {
var monthInterval = taxRecord.RecurringInterval != ReminderMonthInterval.Other ? (int)taxRecord.RecurringInterval : taxRecord.CustomMonthInterval; var monthInterval = taxRecord.RecurringInterval != ReminderMonthInterval.Other ? (int)taxRecord.RecurringInterval : taxRecord.CustomMonthInterval;
return DateTime.Now > taxRecord.Date.AddMonths(monthInterval); bool addDays = taxRecord.RecurringInterval == ReminderMonthInterval.Other && taxRecord.CustomMonthIntervalUnit == ReminderIntervalUnit.Days;
return addDays ? DateTime.Now > taxRecord.Date.AddDays(monthInterval) : DateTime.Now > taxRecord.Date.AddMonths(monthInterval);
} }
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var outdatedRecurringFees = result.Where(x => x.IsRecurring && RecurringTaxIsOutdated(x)); var outdatedRecurringFees = result.Where(x => x.IsRecurring && RecurringTaxIsOutdated(x));
@@ -351,6 +374,7 @@ namespace CarCareTracker.Logic
{ {
var monthInterval = recurringFee.RecurringInterval != ReminderMonthInterval.Other ? (int)recurringFee.RecurringInterval : recurringFee.CustomMonthInterval; var monthInterval = recurringFee.RecurringInterval != ReminderMonthInterval.Other ? (int)recurringFee.RecurringInterval : recurringFee.CustomMonthInterval;
bool isOutdated = true; bool isOutdated = true;
bool addDays = recurringFee.RecurringInterval == ReminderMonthInterval.Other && recurringFee.CustomMonthIntervalUnit == ReminderIntervalUnit.Days;
//update the original outdated tax record //update the original outdated tax record
recurringFee.IsRecurring = false; recurringFee.IsRecurring = false;
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee); _taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
@@ -361,9 +385,9 @@ namespace CarCareTracker.Logic
{ {
try try
{ {
var nextDate = originalDate.AddMonths(monthInterval * monthMultiplier); var nextDate = addDays ? originalDate.AddDays(monthInterval * monthMultiplier) : originalDate.AddMonths(monthInterval * monthMultiplier);
monthMultiplier++; monthMultiplier++;
var nextnextDate = originalDate.AddMonths(monthInterval * monthMultiplier); var nextnextDate = addDays ? originalDate.AddDays(monthInterval * monthMultiplier) : originalDate.AddMonths(monthInterval * monthMultiplier);
recurringFee.Date = nextDate; recurringFee.Date = nextDate;
recurringFee.Id = default; //new record recurringFee.Id = default; //new record
recurringFee.IsRecurring = DateTime.Now <= nextnextDate; recurringFee.IsRecurring = DateTime.Now <= nextnextDate;

View File

@@ -75,7 +75,9 @@ namespace CarCareTracker.Middleware
var userIdentity = new List<Claim> var userIdentity = new List<Claim>
{ {
new(ClaimTypes.Name, splitString[0]), new(ClaimTypes.Name, splitString[0]),
new(ClaimTypes.NameIdentifier, userData.Id.ToString()) new(ClaimTypes.NameIdentifier, userData.Id.ToString()),
new(ClaimTypes.Email, userData.EmailAddress),
new(ClaimTypes.Role, "APIAuth")
}; };
if (userData.IsAdmin) if (userData.IsAdmin)
{ {
@@ -154,6 +156,7 @@ namespace CarCareTracker.Middleware
if (value.ToString().ToLower() == "api") if (value.ToString().ToLower() == "api")
{ {
Response.StatusCode = 401; Response.StatusCode = 401;
Response.Headers.Append("WWW-Authenticate", "Basic");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

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

View File

@@ -23,6 +23,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public List<string> Tags { get; set; } = new List<string>(); public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); 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

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig(); public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0; public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0; public int CustomMonthInterval { get; set; } = 0;
public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date; public ReminderMetric Metric { get; set; } = ReminderMetric.Date;

View File

@@ -13,6 +13,7 @@
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig(); public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0; public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0; public int CustomMonthInterval { get; set; } = 0;
public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date; public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
@@ -34,6 +35,7 @@
ReminderMonthInterval = ReminderMonthInterval, ReminderMonthInterval = ReminderMonthInterval,
CustomMileageInterval = CustomMileageInterval, CustomMileageInterval = CustomMileageInterval,
CustomMonthInterval = CustomMonthInterval, CustomMonthInterval = CustomMonthInterval,
CustomMonthIntervalUnit = CustomMonthIntervalUnit,
Notes = Notes, Notes = Notes,
Tags = Tags Tags = Tags
}; };

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
namespace CarCareTracker.Models
{
public class ServerSettingsViewModel
{
public string LocaleInfo { get; set; }
public string PostgresConnection { get; set; }
public string AllowedFileExtensions { get; set; }
public string CustomLogoURL { get; set; }
public string MessageOfTheDay { get; set; }
public string WebHookURL { get; set; }
public bool CustomWidgetsEnabled { get; set; }
public bool InvariantAPIEnabled { get; set; }
public MailConfig SMTPConfig { get; set; } = new MailConfig();
public OpenIDConfig OIDCConfig { get; set; } = new OpenIDConfig();
}
}

View File

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

View File

@@ -43,7 +43,7 @@ namespace CarCareTracker.Models
public string Cost { get; set; } public string Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
} }
public class GenericRecordExportModel public class GenericRecordExportModel
{ {
@@ -58,7 +58,8 @@ namespace CarCareTracker.Models
[JsonConverter(typeof(FromDecimalOptional))] [JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
public class OdometerRecordExportModel public class OdometerRecordExportModel
{ {
@@ -72,7 +73,8 @@ namespace CarCareTracker.Models
public string Odometer { get; set; } public string Odometer { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
public class TaxRecordExportModel public class TaxRecordExportModel
{ {
@@ -85,7 +87,8 @@ namespace CarCareTracker.Models
[JsonConverter(typeof(FromDecimalOptional))] [JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
public class GasRecordExportModel public class GasRecordExportModel
{ {
@@ -107,10 +110,13 @@ namespace CarCareTracker.Models
public string MissedFuelUp { get; set; } public string MissedFuelUp { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
public class ReminderExportModel public class ReminderExportModel
{ {
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Urgency { get; set; } public string Urgency { get; set; }
public string Metric { get; set; } public string Metric { get; set; }
@@ -119,17 +125,33 @@ namespace CarCareTracker.Models
public string DueDate { get; set; } public string DueDate { get; set; }
[JsonConverter(typeof(FromIntOptional))] [JsonConverter(typeof(FromIntOptional))]
public string DueOdometer { get; set; } public string DueOdometer { get; set; }
public string Tags { get; set; }
} }
public class PlanRecordExportModel public class PlanRecordExportModel
{ {
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateCreated { get; set; } public string DateCreated { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateModified { get; set; } public string DateModified { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Type { get; set; } public string Type { get; set; }
public string Priority { get; set; } public string Priority { get; set; }
public string Progress { get; set; } public string Progress { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; } public string Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class UserExportModel
{
public string Username { get; set; }
public string EmailAddress { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string IsAdmin { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string IsRoot { get; set; }
} }
} }

View File

@@ -0,0 +1,11 @@
namespace CarCareTracker.Models
{
public class StickerViewModel
{
public ImportMode RecordType { get; set; }
public Vehicle VehicleData { get; set; } = new Vehicle();
public List<ReminderRecord> ReminderRecords { get; set; } = new List<ReminderRecord>();
public List<GenericRecord> GenericRecords { get; set; } = new List<GenericRecord>();
public List<SupplyRecord> SupplyRecords { get; set; } = new List<SupplyRecord>();
}
}

View File

@@ -11,6 +11,7 @@
public bool IsRecurring { get; set; } = false; public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear; public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
public int CustomMonthInterval { get; set; } = 0; public int CustomMonthInterval { get; set; } = 0;
public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>(); public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();

View File

@@ -12,6 +12,7 @@
public bool IsRecurring { get; set; } = false; public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths; public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
public int CustomMonthInterval { get; set; } = 0; public int CustomMonthInterval { get; set; } = 0;
public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>(); public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>(); public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
@@ -25,6 +26,7 @@
IsRecurring = IsRecurring, IsRecurring = IsRecurring,
RecurringInterval = RecurringInterval, RecurringInterval = RecurringInterval,
CustomMonthInterval = CustomMonthInterval, CustomMonthInterval = CustomMonthInterval,
CustomMonthIntervalUnit = CustomMonthIntervalUnit,
Files = Files, Files = Files,
Tags = Tags, Tags = Tags,
ExtraFields = ExtraFields ExtraFields = ExtraFields

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,35 @@
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/whoami</code>
</div>
<div class="col-3">
Returns information for current user
</div>
<div class="col-3">
No Params
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/version</code>
</div>
<div class="col-3">
Returns current version of LubeLogger and checks for updates
</div>
<div class="col-3">
CheckForUpdate(bool) - checks for update(optional)
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/vehicles</code> <code>/api/vehicles</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -44,7 +72,7 @@
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/vehicle/info</code> <code>/api/vehicle/info</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -118,6 +146,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -140,6 +169,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -157,6 +187,83 @@
Id - Id of Odometer Record Id - Id of Odometer Record
</div> </div>
</div> </div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords</code>
</div>
<div class="col-3">
Returns a list of plan records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/add</code>
</div>
<div class="col-3">
Adds Plan Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
description - Description<br />
cost - Cost<br />
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
priority - Low/Normal/Critical<br />
progress - Backlog/InProgress/Testing<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/update</code>
</div>
<div class="col-3">
Updates Plan Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Plan Record<br />
description - Description<br />
cost - Cost<br />
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
priority - Low/Normal/Critical<br />
progress - Backlog/InProgress/Testing<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/delete</code>
</div>
<div class="col-3">
Deletes Plan Record
</div>
<div class="col-3">
Id - Id of Plan Record
</div>
</div>
<div class="row api-method"> <div class="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
@@ -192,6 +299,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -215,6 +323,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -267,6 +376,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -290,6 +400,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -342,6 +453,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -365,6 +477,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -400,7 +513,7 @@
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/vehicle/taxrecords/check</code> <code>/api/vehicle/taxrecords/check</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -430,6 +543,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -452,6 +566,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -510,6 +625,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -535,6 +651,7 @@
notes - notes(optional)<br /> notes - notes(optional)<br />
tags - tags separated by space(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 /> 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> </div>
@@ -566,13 +683,102 @@
vehicleId - Id of Vehicle vehicleId - Id of Vehicle
</div> </div>
</div> </div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/add</code>
</div>
<div class="col-3">
Adds Reminder Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/update</code>
</div>
<div class="col-3">
Updates Reminder Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Reminder Record<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/delete</code>
</div>
<div class="col-3">
Deletes Reminder Record
</div>
<div class="col-3">
Id - Id of Reminder Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/calendar</code>
</div>
<div class="col-3">
Returns reminder calendar in ICS format
</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/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))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
<div class="row api-method"> <div class="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/vehicle/reminders/send</code> <code>/api/vehicle/reminders/send</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -580,14 +786,14 @@
</div> </div>
<div class="col-3"> <div class="col-3">
(must be root user)<br /> (must be root user)<br />
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue] urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue](optional)
</div> </div>
</div> </div>
<div class="row api-method"> <div class="row api-method">
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/makebackup</code> <code>/api/makebackup</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -601,7 +807,7 @@
<div class="col-1"> <div class="col-1">
<span class="badge bg-success">GET</span> <span class="badge bg-success">GET</span>
</div> </div>
<div class="col-5 copyable"> <div class="col-5 copyable testable">
<code>/api/cleanup</code> <code>/api/cleanup</code>
</div> </div>
<div class="col-3"> <div class="col-3">
@@ -609,13 +815,20 @@
</div> </div>
<div class="col-3"> <div class="col-3">
(must be root user)<br /> (must be root user)<br />
deepClean(bool) - Perform deep clean deepClean(bool) - Perform deep clean(optional)
</div> </div>
</div> </div>
} }
<script> <script>
$('.copyable').on('click', function (e) { $('.copyable').on('click', function (e) {
if (e.ctrlKey || e.metaKey){
let targetElement = $(e.currentTarget);
if (targetElement.hasClass("testable")){
window.location = targetElement.text().trim();
}
} else {
copyToClipboard(e.currentTarget); copyToClipboard(e.currentTarget);
}
}) })
function showExtraFieldsInfo(){ function showExtraFieldsInfo(){
Swal.fire({ Swal.fire({
@@ -624,4 +837,11 @@
icon: "info" 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> </script>

View File

@@ -13,17 +13,21 @@
emailServerIsSetup = false; emailServerIsSetup = false;
} }
} }
@model AdminViewModel @section Nav {
<div class="container"> <div class="container-fluid">
<div class="row d-flex align-items-center justify-content-between justify-content-md-start"> <div class="row mt-2">
<div class="col-2 col-md-1"> <div class="d-flex lubelogger-navbar">
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a> <div class="me-2" style="cursor:pointer;" onclick="returnToGarage()">
<img src="@config.GetSmallLogoUrl()" class="lubelogger-logo" />
</div> </div>
<div class="col-6 col-md-7 text-end text-md-start"> <span class="text-truncate lead">@translator.Translate(userLanguage, "Admin Panel")</span>
<span class="display-6">@translator.Translate(userLanguage, "Admin Panel")</span>
</div> </div>
</div> </div>
<hr /> <hr />
</div>
}
@model AdminViewModel
<div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="row"> <div class="row">

View File

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

View File

@@ -4,6 +4,7 @@
@model KioskViewModel @model KioskViewModel
@section Scripts { @section Scripts {
<script src="~/lib/masonry/masonry.min.js"></script> <script src="~/lib/masonry/masonry.min.js"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
} }
<div class="progress" role="progressbar" aria-label="Refresh Progress" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="height: 1px"> <div class="progress" role="progressbar" aria-label="Refresh Progress" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="height: 1px">
<div class="progress-bar" style="width: 0%"></div> <div class="progress-bar" style="width: 0%"></div>
@@ -123,9 +124,15 @@
} }
function toggleReminderNote(sender){ function toggleReminderNote(sender){
var reminderNote = $(sender).find('.reminder-note'); var reminderNote = $(sender).find('.reminder-note');
if (reminderNote.text().trim() != ''){ var reminderNoteText = reminderNote.text().trim();
if (reminderNoteText != ''){
if (reminderNote.hasClass('d-none')) { if (reminderNote.hasClass('d-none')) {
reminderNote.removeClass('d-none'); reminderNote.removeClass('d-none');
if (!reminderNote.hasClass('reminder-note-markdown')){
let markedDownReminderNote = markdown(reminderNoteText);
reminderNote.html(markedDownReminderNote);
reminderNote.addClass('reminder-note-markdown');
}
} else { } else {
reminderNote.addClass('d-none'); reminderNote.addClass('d-none');
} }

View File

@@ -36,8 +36,9 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-8">@translator.Translate(userLanguage, "Name")</th> <th scope="col" class="col-5">@translator.Translate(userLanguage, "Name")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Required")</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Required")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Type")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
@@ -47,11 +48,21 @@
@foreach (ExtraField extraField in Model.ExtraFields) @foreach (ExtraField extraField in Model.ExtraFields)
{ {
<script> <script>
extraFields.push({ name: decodeHTMLEntities('@extraField.Name'), isRequired: @extraField.IsRequired.ToString().ToLower()}); extraFields.push({ name: decodeHTMLEntities('@extraField.Name'), isRequired: @extraField.IsRequired.ToString().ToLower(), fieldType: decodeHTMLEntities('@extraField.FieldType')});
</script> </script>
<tr class="d-flex"> <tr class="d-flex">
<td class="col-8">@extraField.Name</td> <td class="col-5">@extraField.Name</td>
<td class="col-2"><input class="form-check-input" type="checkbox" onchange="updateExtraFieldIsRequired(decodeHTMLEntities('@extraField.Name'), this)" value="" @(extraField.IsRequired ? "checked" : "") /></td> <td class="col-2"><input class="form-check-input" type="checkbox" onchange="updateExtraFieldIsRequired(decodeHTMLEntities('@extraField.Name'), this)" value="" @(extraField.IsRequired ? "checked" : "") /></td>
<td class="col-3">
<select class="form-select" onchange="updateExtraFieldType(decodeHTMLEntities('@extraField.Name'), this)">
<!option @(extraField.FieldType == ExtraFieldType.Text ? "selected" : "") value="@((int)ExtraFieldType.Text)">@translator.Translate(userLanguage, "Text")</!option>
<!option @(extraField.FieldType == ExtraFieldType.Number ? "selected" : "") value="@((int)ExtraFieldType.Number)">@translator.Translate(userLanguage, "Number")</!option>
<!option @(extraField.FieldType == ExtraFieldType.Decimal ? "selected" : "") value="@((int)ExtraFieldType.Decimal)">@translator.Translate(userLanguage, "Decimal")</!option>
<!option @(extraField.FieldType == ExtraFieldType.Date ? "selected" : "") value="@((int)ExtraFieldType.Date)">@translator.Translate(userLanguage, "Date")</!option>
<!option @(extraField.FieldType == ExtraFieldType.Time ? "selected" : "") value="@((int)ExtraFieldType.Time)">@translator.Translate(userLanguage, "Time")</!option>
<!option @(extraField.FieldType == ExtraFieldType.Location ? "selected" : "") value="@((int)ExtraFieldType.Location)">@translator.Translate(userLanguage, "Location")</!option>
</select>
</td>
<td class="col-2"><button type="button" onclick="deleteExtraField(decodeHTMLEntities('@extraField.Name'))" class="btn btn-danger"><i class="bi bi-trash"></i></button></td> <td class="col-2"><button type="button" onclick="deleteExtraField(decodeHTMLEntities('@extraField.Name'))" class="btn btn-danger"><i class="bi bi-trash"></i></button></td>
</tr> </tr>
} }
@@ -99,6 +110,12 @@
extraFieldToEdit.isRequired = $(checkbox).is(":checked"); extraFieldToEdit.isRequired = $(checkbox).is(":checked");
updateExtraFields(); updateExtraFields();
} }
function updateExtraFieldType(fieldId, dropDown){
var indexToEdit = extraFields.findIndex(x => x.name == fieldId);
var extraFieldToEdit = extraFields[indexToEdit];
extraFieldToEdit.fieldType = $(dropDown).val();
updateExtraFields();
}
function deleteExtraField(fieldId) { function deleteExtraField(fieldId) {
extraFields = extraFields.filter(x => x.name != fieldId); extraFields = extraFields.filter(x => x.name != fieldId);
updateExtraFields(); updateExtraFields();

View File

@@ -81,9 +81,9 @@
</div> </div>
} }
} }
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-6 garage-item-add"> <div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-6 garage-item-add user-select-none">
<div class="card" onclick="showAddVehicleModal()" style="height:100%;"> <div class="card" onclick="showAddVehicleModal()" style="height:100%;">
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" /> <img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;pointer-events:none;" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,227 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model ServerSettingsViewModel
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="serverConfigModalLabel">@translator.Translate(userLanguage, "Review Server Configurations")</h5>
<button type="button" class="btn-close" onclick="hideServerConfigModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="form-inline">
<div class="form-group">
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputPostgres">@translator.Translate(userLanguage, "Postgres Connection")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputPostgres" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.PostgresConnection">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputFileExt">@translator.Translate(userLanguage, "Allowed File Extensions")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputFileExt" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.AllowedFileExtensions">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputLogoURL">@translator.Translate(userLanguage, "Logo URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputLogoURL" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.CustomLogoURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputMOTD">@translator.Translate(userLanguage, "Message of the Day")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputMOTD" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.MessageOfTheDay">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputWebHook">@translator.Translate(userLanguage, "WebHook URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputWebHook" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.WebHookURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputCustomWidget">@translator.Translate(userLanguage, "Custom Widgets")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputCustomWidget" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.CustomWidgetsEnabled ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputInvariantAPI">@translator.Translate(userLanguage, "Invariant API")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputInvariantAPI" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.InvariantAPIEnabled ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
</div>
</div>
<hr />
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputSMTPServer">@translator.Translate(userLanguage, "SMTP Server")</label>
</div>
<div class="col-md-6 col-12">
<div class="input-group">
<input type="text" readonly id="inputSMTPServer" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.EmailServer">
<div class="input-group-text">
<button type="button" @(string.IsNullOrWhiteSpace(Model.SMTPConfig.EmailServer) ? "disabled" : "") class="btn btn-sm text-secondary password-visible-button" onclick="sendTestEmail()"><i class="bi bi-send"></i></button>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputSMTPPort">@translator.Translate(userLanguage, "SMTP Server Port")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputSMTPPort" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Port">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputSMTPFrom">@translator.Translate(userLanguage, "SMTP Sender Address")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputSMTPFrom" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.EmailFrom">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputSMTPUsername">@translator.Translate(userLanguage, "SMTP Username")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputSMTPUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Username">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputSMTPPassword">@translator.Translate(userLanguage, "SMTP Password")</label>
</div>
<div class="col-md-6 col-12">
<div class="input-group">
<input type="password" readonly id="inputSMTPPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Password">
<div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div>
</div>
</div>
</div>
<hr />
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCProvider">@translator.Translate(userLanguage, "OIDC Provider")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCProvider" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.Name">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCClient">@translator.Translate(userLanguage, "OIDC Client ID")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCClient" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.ClientId">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCSecret">@translator.Translate(userLanguage, "OIDC Client Secret")</label>
</div>
<div class="col-md-6 col-12">
<div class="input-group">
<input type="password" readonly id="inputOIDCSecret" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.ClientSecret">
<div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCAuth">@translator.Translate(userLanguage, "OIDC Auth URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCAuth" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.AuthURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCToken">@translator.Translate(userLanguage, "OIDC Token URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.TokenURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCUserInfo">@translator.Translate(userLanguage, "OIDC UserInfo URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCUserInfo" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.UserInfoURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCRedirect">@translator.Translate(userLanguage, "OIDC Redirect URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCRedirect" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.RedirectURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCScope">@translator.Translate(userLanguage, "OIDC Scope")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCScope" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.Scope">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCLogout">@translator.Translate(userLanguage, "OIDC Logout URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCLogout" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.LogOutURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCState">@translator.Translate(userLanguage, "OIDC Validate State")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCState" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.ValidateState ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCPKCE">@translator.Translate(userLanguage, "OIDC Use PKCE")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCPKCE" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.UsePKCE ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCDisable">@translator.Translate(userLanguage, "OIDC Login Only")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCDisable" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.DisableRegularLogin ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
</div>
</div>
</div>
</form>
</div>

View File

@@ -86,6 +86,14 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies">
<label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label> <label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label>
</div> </div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showCalendar" checked="@Model.UserConfig.ShowCalendar">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Calendar")</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showVehicleThumbnail" checked="@Model.UserConfig.ShowVehicleThumbnail">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Vehicle Thumbnail in Header")</label>
</div>
</div> </div>
@if (User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
@@ -253,7 +261,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="row">
<div class="col-10">
<span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span> <span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span>
</div>
<div class="col-2">
<button onclick="showServerConfigModal()" class="btn text-secondary btn-sm"><i class="bi bi-eyeglasses"></i></button>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-6 d-grid"> <div class="col-6 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button> <button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button>
@@ -355,6 +370,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" data-bs-focus="false" id="serverConfigModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="serverConfigModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="tabReorderModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="tabReorderModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="tabReorderModalContent"> <div class="modal-content" id="tabReorderModalContent">

View File

@@ -16,7 +16,7 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control"> <input type="text" onkeyup="callBackOnEnter(event, requestPasswordReset)" id="inputUserName" class="form-control">
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Request")</button> <button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Request")</button>

View File

@@ -24,7 +24,7 @@
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control"> <input type="password" id="inputUserPassword" onkeyup="callBackOnEnter(event, performLogin)" class="form-control">
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button> <button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div> </div>

View File

@@ -3,6 +3,7 @@
@inject ITranslationHelper translator @inject ITranslationHelper translator
@{ @{
var userLanguage = config.GetServerLanguage(); var userLanguage = config.GetServerLanguage();
var openRegistrationEnabled = config.GetServerOpenRegistration();
} }
@model string @model string
@{ @{
@@ -17,11 +18,23 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group"> <div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label> <label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
@if (openRegistrationEnabled)
{
<div class="input-group">
<input type="text" id="inputToken" class="form-control"> <input type="text" id="inputToken" class="form-control">
<div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="sendOpenIdRegistrationToken()"><i class="bi bi-send"></i></button>
</div>
</div>
}
else
{
<input type="text" id="inputToken" class="form-control">
}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control" value="@Model"> <input type="text" id="inputUserName" class="form-control" value="@Model" onkeyup="callBackOnEnter(event, performOpenIdRegistration)">
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performOpenIdRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button> <button type="button" class="btn btn-warning mt-2" onclick="performOpenIdRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
@@ -46,4 +59,14 @@
} }
}); });
} }
function sendOpenIdRegistrationToken(){
var userEmail = decodeHTMLEntities('@Model');
$.post('/Login/SendRegistrationToken', { emailAddress: userEmail }, function (data) {
if (data.success) {
successToast(data.message);
} else {
errorToast(data.message);
}
});
}
</script> </script>

View File

@@ -3,7 +3,9 @@
@inject ITranslationHelper translator @inject ITranslationHelper translator
@{ @{
var userLanguage = config.GetServerLanguage(); var userLanguage = config.GetServerLanguage();
var openRegistrationEnabled = config.GetServerOpenRegistration();
} }
@model LoginModel
@{ @{
ViewData["Title"] = "Register"; ViewData["Title"] = "Register";
} }
@@ -16,11 +18,20 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" /> <img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group"> <div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label> <label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control"> @if (openRegistrationEnabled) {
<div class="input-group">
<input type="text" id="inputToken" class="form-control" value="@Model.Token">
<div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="sendRegistrationToken()"><i class="bi bi-send"></i></button>
</div>
</div>
} else {
<input type="text" id="inputToken" class="form-control" value="@Model.Token">
}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Email Address")</label> <label for="inputEmail">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control"> <input type="text" id="inputEmail" class="form-control" value="@Model.EmailAddress">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
@@ -29,7 +40,7 @@
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="inputUserPassword" class="form-control"> <input type="password" id="inputUserPassword" class="form-control" onkeyup="callBackOnEnter(event, performRegistration)">
<div class="input-group-text"> <div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button> <button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
</div> </div>

View File

@@ -0,0 +1,17 @@
@model List<OperationResponse>
@{
ViewData["Title"] = "Remote Auth Debug";
}
<div class="mt-2">
@foreach (OperationResponse result in Model)
{
<div class="row">
<div class="col-12">
<div class="alert text-wrap text-break @(result.Success ? "alert-success" : "alert-danger")" role="alert">
@result.Message
</div>
</div>
</div>
}
</div>

View File

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

View File

@@ -34,6 +34,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>@ViewData["Title"] - LubeLogger</title> <title>@ViewData["Title"] - LubeLogger</title>
<link rel="icon" type="image/x-icon" href="~/favicon.ico">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
@@ -160,6 +161,7 @@
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</head> </head>
<body> <body>
@await RenderSectionAsync("Nav", required: false)
<div class="container" style="height:85vh;"> <div class="container" style="height:85vh;">
<main role="main"> <main role="main">
@RenderBody() @RenderBody()

View File

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

View File

@@ -0,0 +1,9 @@
@model List<UploadedFiles>
@if (Model.Any()){
<span class="position-relative">
<i class="bi bi-paperclip"></i>
<span class="position-absolute attachment-badge-xs start-100 translate-middle badge rounded-pill bg-secondary">
@Model.Count()
</span>
</span>
}

View File

@@ -21,30 +21,7 @@
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
@translator.Translate(userLanguage, "Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.") @translator.Translate(userLanguage, "Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.")
</div> </div>
@if (Model == ImportMode.GasRecord) <a class="btn btn-link" href="@($"/Vehicle/GenerateCsvSample?mode={Model.ToString()}")" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
{
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
{
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.TaxRecord)
{
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.SupplyRecord)
{
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.PlanRecord)
{
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.OdometerRecord)
{
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<a onclick="showRecurringReminderSelector('collisionRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a> <a onclick="showRecurringReminderSelector('collisionRecordDescription', 'collisionRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div> </div>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="collisionRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="collisionRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -54,31 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Odometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Description" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -87,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('RepairRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -118,6 +124,7 @@
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -133,6 +140,7 @@
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(collisionRecord.Mileage == default ? "---" : collisionRecord.Mileage.ToString())</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(collisionRecord.Mileage == default ? "---" : collisionRecord.Mileage.ToString())</td>
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@collisionRecord.Description</td> <td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@collisionRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(collisionRecord.Cost, hideZero))</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(collisionRecord.Cost, hideZero))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", collisionRecord.Files)</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(collisionRecord.Notes)</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -185,6 +193,8 @@
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'RepairRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li> <li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li> <li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li>

View File

@@ -65,9 +65,11 @@
var yearDataToDisplay = Model.CostData.Where(x => x.Year == year); var yearDataToDisplay = Model.CostData.Where(x => x.Year == year);
if (yearDataToDisplay != null && yearDataToDisplay != default) if (yearDataToDisplay != null && yearDataToDisplay != default)
{ {
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" report-data="cost">@(StaticHelper.HideZeroCost(yearDataToDisplay.Sum(x => x.Cost).ToString("C2"), hideZero))</td> var distanceTraveled = yearDataToDisplay.Sum(x => x.DistanceTraveled);
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate d-none" report-data="distance">@(yearDataToDisplay.Sum(x => x.DistanceTraveled) != default ? $"{yearDataToDisplay.Sum(x => x.DistanceTraveled).ToString("N0")} {Model.DistanceUnit}" : "---")</td> var costAccrued = yearDataToDisplay.Sum(x => x.Cost);
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate d-none" report-data="costperdistance">@(StaticHelper.HideZeroCost(yearDataToDisplay.Sum(x => x.CostPerDistanceTraveled).ToString("C2"), hideZero, $"/{Model.DistanceUnit}"))</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" report-data="cost">@(StaticHelper.HideZeroCost(costAccrued.ToString("C2"), hideZero))</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate d-none" report-data="distance">@(distanceTraveled != default ? $"{distanceTraveled.ToString("N0")} {Model.DistanceUnit}" : "---")</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate d-none" report-data="costperdistance">@(StaticHelper.HideZeroCost(distanceTraveled != default && costAccrued != default ? (costAccrued / distanceTraveled).ToString("C2") : 0M.ToString("C2"), hideZero, $"/{Model.DistanceUnit}"))</td>
} }
else else
{ {

View File

@@ -21,8 +21,8 @@
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Type")</th> <th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Type")</th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Cost Per Day")</th> <th scope="col" style="cursor:pointer;" onclick="toggleCostTableHint()" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Cost Per Day")<span class="cost-table-hint d-none">@($"({Model.NumberOfDays.ToString("N0")})")</span></th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, Model.DistanceUnit)</th> <th scope="col" style="cursor:pointer;" onclick="toggleCostTableHint()" class="col-3 flex-grow-1">@translator.Translate(userLanguage, Model.DistanceUnit)<span class="cost-table-hint d-none">@($"({Model.TotalDistance.ToString("N0")})")</span></th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</th> <th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</th>
</tr> </tr>
</thead> </thead>

View File

@@ -0,0 +1,42 @@
@model List<ExtraField>
@if (Model.Any()){
@foreach (ExtraField field in Model)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
@switch(field.FieldType){
case (ExtraFieldType.Text):
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
break;
case (ExtraFieldType.Number):
<input type="number" inputmode="numeric" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
break;
case (ExtraFieldType.Decimal):
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
break;
case (ExtraFieldType.Date):
<div class="input-group">
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<script>initExtraFieldDatePicker('@elementId')</script>
break;
case (ExtraFieldType.Time):
<input type="time" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
break;
case (ExtraFieldType.Location):
<div class="input-group">
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="populateLocationField('@elementId')"><i class="bi bi-geo-alt"></i></button>
</div>
</div>
break;
default:
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
break;
}
</div>
}
}

View File

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

View File

@@ -12,7 +12,7 @@
{ {
<li class="list-group-item"> <li class="list-group-item">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a> <a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" title="@filesUploaded.Name" target="_blank">@filesUploaded.Name</a>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button> <button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button> <button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>

View File

@@ -104,49 +104,55 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='daterefueled' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_DateRefueled" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='daterefueled' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_DateRefueled" checked>
<label class="form-check-label stretched-link" for="chkCol_DateRefueled">@translator.Translate(userLanguage, "Date Refueled")</label> <label class="form-check-label stretched-link" for="chkCol_DateRefueled">@translator.Translate(userLanguage, "Date Refueled")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Odometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='delta' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Delta" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='delta' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Delta" checked>
<label class="form-check-label stretched-link" for="chkCol_Delta">@translator.Translate(userLanguage, "Delta")</label> <label class="form-check-label stretched-link" for="chkCol_Delta">@translator.Translate(userLanguage, "Delta")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='consumption' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Consumption" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='consumption' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Consumption" checked>
<label class="form-check-label stretched-link" for="chkCol_Consumption">@translator.Translate(userLanguage, "Consumption")</label> <label class="form-check-label stretched-link" for="chkCol_Consumption">@translator.Translate(userLanguage, "Consumption")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='fueleconomy' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_FuelEconomy" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='fueleconomy' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_FuelEconomy" checked>
<label class="form-check-label stretched-link" for="chkCol_FuelEconomy">@translator.Translate(userLanguage, "Fuel Economy")</label> <label class="form-check-label stretched-link" for="chkCol_FuelEconomy">@translator.Translate(userLanguage, "Fuel Economy")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='unitcost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_UnitCost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='unitcost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_UnitCost" checked>
<label class="form-check-label stretched-link" for="chkCol_UnitCost">@translator.Translate(userLanguage, "Unit Cost")</label> <label class="form-check-label stretched-link" for="chkCol_UnitCost">@translator.Translate(userLanguage, "Unit Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Notes"> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Notes">
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -155,7 +161,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('GasRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -188,6 +194,7 @@
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Unit Cost")</th> <th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Unit Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -206,6 +213,7 @@
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td> <td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td> <td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", gasRecord.Files)</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@StaticHelper.TruncateStrings(gasRecord.Notes)</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@StaticHelper.TruncateStrings(gasRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -254,6 +262,8 @@
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
<li><hr class="context-menu-odometer-adjustment dropdown-divider"></li> <li><hr class="context-menu-odometer-adjustment dropdown-divider"></li>
<li><a class="context-menu-odometer-adjustment dropdown-item" href="#" onclick="adjustRecordsOdometer(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Adjust Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="context-menu-odometer-adjustment dropdown-item" href="#" onclick="adjustRecordsOdometer(selectedRow, 'GasRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Adjust Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>

View File

@@ -97,14 +97,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.GasRecord.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.GasRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

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

View File

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

View File

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

View File

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

View File

@@ -109,5 +109,8 @@
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'NoteRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
</ul> </ul>

View File

@@ -72,14 +72,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="odometerRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="odometerRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -54,31 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='initialodometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_InitialOdometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='initialodometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_InitialOdometer" checked>
<label class="form-check-label stretched-link" for="chkCol_InitialOdometer">@translator.Translate(userLanguage, "Initial Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_InitialOdometer">@translator.Translate(userLanguage, "Initial Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Odometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='distance' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Distance" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='distance' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Distance" checked>
<label class="form-check-label stretched-link" for="chkCol_Distance">@translator.Translate(userLanguage, "Distance")</label> <label class="form-check-label stretched-link" for="chkCol_Distance">@translator.Translate(userLanguage, "Distance")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -87,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('OdometerRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -118,6 +124,7 @@
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@translator.Translate(userLanguage, "Initial Odometer")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@translator.Translate(userLanguage, "Initial Odometer")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" onclick="toggleSort('odometer-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Distance")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" onclick="toggleSort('odometer-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Distance")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -131,8 +138,9 @@
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@odometerRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditOdometerRecordModal,@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'> <tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@odometerRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditOdometerRecordModal,@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date">@odometerRecord.Date.ToShortDateString()</td> <td class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date">@odometerRecord.Date.ToShortDateString()</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@odometerRecord.InitialMileage</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@odometerRecord.InitialMileage</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@odometerRecord.Mileage</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer" data-record-type="cost">@odometerRecord.Mileage</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" data-record-type="distance">@(odometerRecord.DistanceTraveled == default ? "---" : odometerRecord.DistanceTraveled)</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" data-record-type="distance">@(odometerRecord.DistanceTraveled == default ? "---" : odometerRecord.DistanceTraveled)</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", odometerRecord.Files)</td>
<td class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td> <td class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -181,6 +189,9 @@
<li><hr class="context-menu-multiple dropdown-divider"></li> <li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
<li><hr class="context-menu-odometer-adjustment dropdown-divider"></li> <li><hr class="context-menu-odometer-adjustment dropdown-divider"></li>
<li><a class="context-menu-odometer-adjustment dropdown-item" href="#" onclick="adjustRecordsOdometer(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Adjust Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="context-menu-odometer-adjustment dropdown-item" href="#" onclick="adjustRecordsOdometer(selectedRow, 'OdometerRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Adjust Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>

View File

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

View File

@@ -40,14 +40,7 @@
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option> <!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option> <!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
@if (!isNew) @if (!isNew)
{ {
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label> <label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>

View File

@@ -40,14 +40,7 @@
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option> <!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option> <!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="planRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="planRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="row swimlane"> <div class="row swimlane">
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')"> <div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Planned")</span> <span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
@@ -59,7 +59,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord) @await Html.PartialAsync("_PlanRecordItem", planRecord)
} }
</div> </div>
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')"> <div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Doing")</span> <span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
@@ -81,7 +81,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord) @await Html.PartialAsync("_PlanRecordItem", planRecord)
} }
</div> </div>
<div class="col-3 d-flex flex-column swimlane end" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')"> <div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Done")</span> <span class="display-7">@translator.Translate(userLanguage,"Done")</span>
@@ -127,5 +127,8 @@
<li><hr class="context-menu-move move-header dropdown-divider"></li> <li><hr class="context-menu-move move-header dropdown-divider"></li>
<li><a class="dropdown-item context-menu-move move-header context-menu-duplicate" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item context-menu-move move-header context-menu-duplicate" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item context-menu-move move-header context-menu-duplicate-vehicle" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item context-menu-move move-header context-menu-duplicate-vehicle" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="context-menu-move move-header dropdown-divider"></li>
<li><a class="dropdown-item context-menu-move move-header context-menu-print-tab-sticker" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="context-menu-move move-header dropdown-divider"></li>
<li><a class="dropdown-item context-menu-move move-header text-danger context-menu-delete" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item context-menu-move move-header text-danger context-menu-delete" href="#"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
</ul> </ul>

View File

@@ -5,7 +5,7 @@
var userLanguage = userConfig.UserLanguage; var userLanguage = userConfig.UserLanguage;
} }
@using CarCareTracker.Helper @using CarCareTracker.Helper
@model List<ReminderRecord> @model List<ReminderRecordViewModel>
@if (Model.Count() > 1) @if (Model.Count() > 1)
{ {
<div class="mb-2"> <div class="mb-2">
@@ -16,9 +16,25 @@
<select class="form-select" id="recurringReminderInput"> <select class="form-select" id="recurringReminderInput">
@if (Model.Any()) @if (Model.Any())
{ {
@foreach (ReminderRecord reminderRecord in Model) @foreach (ReminderRecordViewModel reminderRecord in Model)
{ {
<!option value="@reminderRecord.Id">@reminderRecord.Description</!option> @switch(reminderRecord.UserMetric){
case (ReminderMetric.Both):
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
@($"{reminderRecord.Description} | {reminderRecord.Date.ToShortDateString()} | {reminderRecord.Mileage}")
</!option>
break;
case (ReminderMetric.Odometer):
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
@($"{reminderRecord.Description} | {reminderRecord.Mileage}")
</!option>
break;
case (ReminderMetric.Date):
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
@($"{reminderRecord.Description} | {reminderRecord.Date.ToShortDateString()}")
</!option>
break;
}
} }
} else } else
{ {
@@ -27,11 +43,26 @@
</select> </select>
<div id="recurringMultipleReminders" style="display:none;"> <div id="recurringMultipleReminders" style="display:none;">
<ul class="list-group"> <ul class="list-group">
@foreach (ReminderRecord reminderRecord in Model) @foreach (ReminderRecordViewModel reminderRecord in Model)
{ {
<li class="list-group-item text-start"> <li class="list-group-item text-start">
<input class="form-check-input" type="checkbox" value="@reminderRecord.Id" id="recurringReminder_@reminderRecord.Id"> <input class="form-check-input" type="checkbox" value="@reminderRecord.Id" data-description="@reminderRecord.Description" id="recurringReminder_@reminderRecord.Id">
<label class="form-check-label stretched-link" for="recurringReminder_@reminderRecord.Id">@reminderRecord.Description</label> <label class="form-check-label stretched-link" for="recurringReminder_@reminderRecord.Id">
@reminderRecord.Description
<br /><small class="badge @StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
@switch (reminderRecord.UserMetric){
case (ReminderMetric.Both):
<i class='bi bi-calendar-event me-2'></i>@reminderRecord.Date.ToShortDateString()<i class='bi bi-speedometer ms-2 me-2'></i>@reminderRecord.Mileage
break;
case (ReminderMetric.Odometer):
<i class='bi bi-speedometer me-2'></i>@reminderRecord.Mileage
break;
case (ReminderMetric.Date):
<i class='bi bi-calendar-event me-2'></i>@reminderRecord.Date.ToShortDateString()
break;
}
</small>
</label>
</li> </li>
} }
</ul> </ul>

View File

@@ -82,9 +82,9 @@
<!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option> <!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option>
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option> <!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
</select> </select>
<label for="reminderRecurringMonth">@translator.Translate(userLanguage, "Month")</label> <label for="reminderRecurringMonth">@translator.Translate(userLanguage, "Time")</label>
<select class="form-select" onchange="checkCustomMonthInterval()" id="reminderRecurringMonth" @(Model.IsRecurring && (Model.Metric == ReminderMetric.Date || Model.Metric == ReminderMetric.Both) ? "" : "disabled")> <select class="form-select" onchange="checkCustomMonthInterval()" id="reminderRecurringMonth" @(Model.IsRecurring && (Model.Metric == ReminderMetric.Date || Model.Metric == ReminderMetric.Both) ? "" : "disabled")>
<!option value="Other" @(Model.ReminderMonthInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.ReminderMonthInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option> <!option value="Other" @(Model.ReminderMonthInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.ReminderMonthInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval} {Model.CustomMonthIntervalUnit}" : $"{translator.Translate(userLanguage, "Other")}") </!option>
<!option value="OneMonth" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage, "1 Month")</!option> <!option value="OneMonth" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage, "1 Month")</!option>
<!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage,"3 Months")</!option> <!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage,"3 Months")</!option>
<!option value="SixMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage,"6 Months")</!option> <!option value="SixMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage,"6 Months")</!option>
@@ -136,6 +136,7 @@
<script> <script>
var customMileageInterval = @Model.CustomMileageInterval; var customMileageInterval = @Model.CustomMileageInterval;
var customMonthInterval = @Model.CustomMonthInterval; var customMonthInterval = @Model.CustomMonthInterval;
var customMonthIntervalUnit = decodeHTMLEntities('@Model.CustomMonthIntervalUnit');
function getReminderRecordModelData() { function getReminderRecordModelData() {
return { id: @Model.Id, mileageInterval: decodeHTMLEntities('@Model.ReminderMileageInterval.ToString()'), monthInterval: decodeHTMLEntities('@Model.ReminderMonthInterval.ToString()')} return { id: @Model.Id, mileageInterval: decodeHTMLEntities('@Model.ReminderMileageInterval.ToString()'), monthInterval: decodeHTMLEntities('@Model.ReminderMonthInterval.ToString()')}
} }

View File

@@ -85,6 +85,20 @@
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div> </div>
</li> </li>
@if(hasRefresh){
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='done' onChange="showTableColumns(this, 'ReminderRecord')" type="checkbox" id="chkCol_Done" checked>
<label class="form-check-label stretched-link" for="chkCol_Done">@translator.Translate(userLanguage, "Done")</label>
</div>
</li>
}
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='delete' onChange="showTableColumns(this, 'ReminderRecord')" type="checkbox" id="chkCol_Delete" checked>
<label class="form-check-label stretched-link" for="chkCol_Delete">@translator.Translate(userLanguage, "Delete")</label>
</div>
</li>
</ul> </ul>
</div> </div>
} }
@@ -113,9 +127,9 @@
<th scope="col" data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@translator.Translate(userLanguage, "Notes")</th>
@if (hasRefresh) @if (hasRefresh)
{ {
<th scope="col" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Done")</th> <th scope="col" data-column="done" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Done")</th>
} }
<th scope="col" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Delete")</th> <th scope="col" data-column="delete" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -140,7 +154,7 @@
<span class="badge text-bg-success">@translator.Translate(userLanguage, "Not Urgent")</span> <span class="badge text-bg-success">@translator.Translate(userLanguage, "Not Urgent")</span>
} }
</td> </td>
<td class="col-2 text-truncate flex-grow-1 flex-shrink-1" data-column="metric"> <td class="col-2 text-truncate flex-grow-1 flex-shrink-1" data-column="metric" data-record-type="cost">
@if (reminderRecord.Metric == ReminderMetric.Date) @if (reminderRecord.Metric == ReminderMetric.Date)
{ {
@reminderRecord.Date.ToShortDateString() @reminderRecord.Date.ToShortDateString()
@@ -164,14 +178,14 @@
<td data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@StaticHelper.TruncateStrings(reminderRecord.Notes)</td> <td data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@StaticHelper.TruncateStrings(reminderRecord.Notes)</td>
@if (hasRefresh) @if (hasRefresh)
{ {
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint"> <td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint" data-column="done">
@if((reminderRecord.Urgency == ReminderUrgency.VeryUrgent || reminderRecord.Urgency == ReminderUrgency.PastDue) && reminderRecord.IsRecurring) @if((reminderRecord.Urgency == ReminderUrgency.VeryUrgent || reminderRecord.Urgency == ReminderUrgency.PastDue) && reminderRecord.IsRecurring)
{ {
<button type="button" class="btn btn-secondary" onclick="markDoneReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-check-lg"></i></button> <button type="button" class="btn btn-secondary" onclick="markDoneReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-check-lg"></i></button>
} }
</td> </td>
} }
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint"> <td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint" data-column="delete">
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button> <button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button>
</td> </td>
</tr> </tr>
@@ -200,6 +214,9 @@
<li><hr class="context-menu-multiple dropdown-divider"></li> <li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
</ul> </ul>

View File

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

View File

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

View File

@@ -25,6 +25,14 @@
} }
</ul> </ul>
</div> </div>
<div class="mt-2 mb-2">
<ul class="list-group">
<li class="list-group-item text-start">
<input class="form-check-input" type="checkbox" role="switch" id="printIndividualRecordsCheck">
<label class="form-check-label" for="printIndividualRecordsCheck">@translator.Translate(userLanguage, "Print Individual Records")</label>
</li>
</ul>
</div>
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item text-center" style="cursor:pointer;" onclick="showReportAdvancedParameters()"> <li class="list-group-item text-center" style="cursor:pointer;" onclick="showReportAdvancedParameters()">

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<a onclick="showRecurringReminderSelector('serviceRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a> <a onclick="showRecurringReminderSelector('serviceRecordDescription', 'serviceRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div> </div>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -54,31 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Odometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Description" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -87,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('ServiceRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -118,6 +124,7 @@
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('servicerecord-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('servicerecord-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -133,6 +140,7 @@
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(serviceRecord.Mileage == default ? "---" : serviceRecord.Mileage.ToString())</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(serviceRecord.Mileage == default ? "---" : serviceRecord.Mileage.ToString())</td>
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@serviceRecord.Description</td> <td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@serviceRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(serviceRecord.Cost, hideZero))</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(serviceRecord.Cost, hideZero))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", serviceRecord.Files)</td>
<td class="col-3 text-truncate flex-grow-1 flex-shrink-1" data-column="notes">@StaticHelper.TruncateStrings(serviceRecord.Notes)</td> <td class="col-3 text-truncate flex-grow-1 flex-shrink-1" data-column="notes">@StaticHelper.TruncateStrings(serviceRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -183,6 +191,8 @@
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'ServiceRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li> <li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li> <li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li>

View File

@@ -0,0 +1,277 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model StickerViewModel
@{
var userConfig = config.GetUserConfig(User);
var hideZero = userConfig.HideZero;
var userLanguage = userConfig.UserLanguage;
}
@if( Model.ReminderRecords.Any()){
@foreach(ReminderRecord reminder in Model.ReminderRecords){
<div class="reminderSticker">
<div class="row justify-content-center mt-2">
<img src="@config.GetLogoUrl()" class="lubelogger-logo-sticker" />
</div>
<div class="row">
<div class="col-12 text-center">
<p class="display-1">@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")</p>
</div>
</div>
<div class="row">
<div class="col-12 text-center">
<p class="display-2">@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")</p>
</div>
</div>
<div class="row">
<div class="col-12 text-center">
<p class="display-2 text-uppercase fw-bold">@($"{reminder.Description}")</p>
</div>
</div>
@if (reminder.Metric == ReminderMetric.Odometer || reminder.Metric == ReminderMetric.Both)
{
<div class="row">
<div class="col-12 text-center">
<p class="display-2">@($"{translator.Translate(userLanguage, "Odometer")}")</p>
</div>
</div>
<div class="row">
<div class="col-12 text-center">
<p class="display-2 fw-bold">@($"{reminder.Mileage}")</p>
</div>
</div>
}
@if (reminder.Metric == ReminderMetric.Date || reminder.Metric == ReminderMetric.Both)
{
<div class="row">
<div class="col-12 text-center">
<p class="display-2">@($"{translator.Translate(userLanguage, "Date")}")</p>
</div>
</div>
<div class="row">
<div class="col-12 text-center">
<p class="display-2 fw-bold">@($"{reminder.Date.ToShortDateString()}")</p>
</div>
</div>
}
@if (reminder.Metric == ReminderMetric.Both)
{
<div class="row">
<div class="col-12 text-center">
<p class="display-2 text-uppercase">@($"{translator.Translate(userLanguage, "Whichever comes first")}")</p>
</div>
</div>
}
</div>
}
} else if (Model.GenericRecords.Any()){
@foreach(GenericRecord genericRecord in Model.GenericRecords){
<div class="d-flex flex-column recordSticker">
<div class="d-flex">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
</div>
<hr />
<div class="row">
<div class="col-6">
<ul class="list-group">
<li class="list-group-item">
<span class="display-6">@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")</span>
</li>
<li class="list-group-item">
<span class="lead">@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")</span>
</li>
@foreach (ExtraField extraField in Model.VehicleData.ExtraFields)
{
if (!string.IsNullOrWhiteSpace(extraField.Value))
{
<li class="list-group-item">
<span class="lead">@($"{extraField.Name}: {extraField.Value}")</span>
</li>
}
}
</ul>
</div>
<div class="col-6">
<ul class="list-group">
@if(!string.IsNullOrWhiteSpace(genericRecord.Description)){
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Description")}: {genericRecord.Description}")
</li>
}
@switch(Model.RecordType){
case ImportMode.ServiceRecord:
case ImportMode.RepairRecord:
case ImportMode.UpgradeRecord:
case ImportMode.GasRecord:
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Mileage}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
</li>
break;
case ImportMode.TaxRecord:
case ImportMode.PlanRecord:
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
</li>
break;
case ImportMode.OdometerRecord:
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Mileage}")
</li>
break;
}
@foreach(ExtraField extraField in genericRecord.ExtraFields){
<li class="list-group-item">
@($"{extraField.Name}: {extraField.Value}")
</li>
}
</ul>
</div>
</div>
<hr />
@if(genericRecord.RequisitionHistory.Any()){
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part Number")</th>
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Cost")</th>
</tr>
</thead>
<tbody>
@foreach (SupplyUsageHistory usageHistory in genericRecord.RequisitionHistory)
{
<tr class="d-flex">
<td class="col-2 text-truncate">@usageHistory.PartNumber</td>
<td class="col-6 text-truncate">@usageHistory.Description</td>
<td class="col-2">@usageHistory.Quantity.ToString("F")</td>
<td class="col-2">@usageHistory.Cost.ToString("C2")</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<hr />
}
<div class="row flex-grow-1 flex-shrink-1">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(genericRecord.Notes)
</div>
</div>
</div>
</div>
}
<script>setMarkDownStickerNotes()</script>
} else if (Model.SupplyRecords.Any()){
@foreach (SupplyRecord supplyRecord in Model.SupplyRecords)
{
<div class="d-flex flex-column recordSticker">
<div class="d-flex">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
</div>
<hr />
<div class="row">
@if(Model.VehicleData.Id != default){
<div class="col-6">
<ul class="list-group">
<li class="list-group-item">
<span class="display-6">@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")</span>
</li>
<li class="list-group-item">
<span class="lead">@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")</span>
</li>
@foreach (ExtraField extraField in Model.VehicleData.ExtraFields)
{
if (!string.IsNullOrWhiteSpace(extraField.Value))
{
<li class="list-group-item">
<span class="lead">@($"{extraField.Name}: {extraField.Value}")</span>
</li>
}
}
</ul>
</div>
<div class="col-6">
<ul class="list-group">
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Description")}: {supplyRecord.Description}")
</li>
@if(!string.IsNullOrWhiteSpace(supplyRecord.PartNumber)){
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Part Number")}: {supplyRecord.PartNumber}")
</li>
}
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Supplier/Vendor")}: {supplyRecord.PartSupplier}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {supplyRecord.Cost.ToString("C")}")
</li>
@foreach (ExtraField extraField in supplyRecord.ExtraFields)
{
<li class="list-group-item">
@($"{extraField.Name}: {extraField.Value}")
</li>
}
</ul>
</div>
} else {
<div class="col-6">
<ul class="list-group">
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Description")}: {supplyRecord.Description}")
</li>
@if (!string.IsNullOrWhiteSpace(supplyRecord.PartNumber))
{
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Part Number")}: {supplyRecord.PartNumber}")
</li>
}
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Supplier/Vendor")}: {supplyRecord.PartSupplier}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {supplyRecord.Cost.ToString("C")}")
</li>
</ul>
</div>
<div class="col-6">
<ul class="list-group">
@foreach (ExtraField extraField in supplyRecord.ExtraFields)
{
<li class="list-group-item">
@($"{extraField.Name}: {extraField.Value}")
</li>
}
</ul>
</div>
}
</div>
<hr />
<div class="row flex-grow-1 flex-shrink-1">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(supplyRecord.Notes)
</div>
</div>
</div>
</div>
}
<script>setMarkDownStickerNotes()</script>
}

View File

@@ -50,14 +50,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="supplyRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="supplyRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -54,43 +54,49 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='partnumber' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_PartNumber" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='partnumber' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_PartNumber" checked>
<label class="form-check-label stretched-link" for="chkCol_PartNumber">@translator.Translate(userLanguage, "Part Number")</label> <label class="form-check-label stretched-link" for="chkCol_PartNumber">@translator.Translate(userLanguage, "Part Number")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='supplier' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Supplier" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='supplier' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Supplier" checked>
<label class="form-check-label stretched-link" for="chkCol_Supplier">@translator.Translate(userLanguage, "Supplier")</label> <label class="form-check-label stretched-link" for="chkCol_Supplier">@translator.Translate(userLanguage, "Supplier")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Description" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='quantity' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Quantity" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='quantity' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Quantity" checked>
<label class="form-check-label stretched-link" for="chkCol_Quantity">@translator.Translate(userLanguage, "Quantity")</label> <label class="form-check-label stretched-link" for="chkCol_Quantity">@translator.Translate(userLanguage, "Quantity")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -99,7 +105,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('SupplyRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -132,6 +138,7 @@
<th scope="col" class="col-2 flex-grow-1 col-xl-3 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-2 flex-grow-1 col-xl-3 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="quantity" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Quantity")</th> <th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="quantity" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -149,6 +156,7 @@
<td class="col-2 flex-grow-1 col-xl-3 text-truncate" data-column="description">@supplyRecord.Description</td> <td class="col-2 flex-grow-1 col-xl-3 text-truncate" data-column="description">@supplyRecord.Description</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="quantity">@supplyRecord.Quantity</td> <td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="quantity">@supplyRecord.Quantity</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(supplyRecord.Cost, hideZero))</td> <td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(supplyRecord.Cost, hideZero))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", supplyRecord.Files)</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(supplyRecord.Notes)</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(supplyRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -194,6 +202,9 @@
<li><hr class="context-menu-multiple dropdown-divider"></li> <li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'SupplyRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
</ul> </ul>
@if (userColumnPreferences.Any()) @if (userColumnPreferences.Any())

View File

@@ -28,7 +28,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<a onclick="showRecurringReminderSelector('taxRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a> <a onclick="showRecurringReminderSelector('taxRecordDescription', 'taxRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div> </div>
</div> </div>
} }
@@ -41,14 +41,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -57,9 +50,9 @@
<input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring"> <input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring">
<label class="form-check-label" for="taxIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label> <label class="form-check-label" for="taxIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label>
</div> </div>
<label for="taxRecurringMonth">@translator.Translate(userLanguage,"Month")</label> <label for="taxRecurringMonth">@translator.Translate(userLanguage,"Time")</label>
<select class="form-select" onchange="checkCustomMonthIntervalForTax()" id="taxRecurringMonth" @(Model.IsRecurring ? "" : "disabled")> <select class="form-select" onchange="checkCustomMonthIntervalForTax()" id="taxRecurringMonth" @(Model.IsRecurring ? "" : "disabled")>
<!option value="Other" @(Model.RecurringInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.RecurringInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option> <!option value="Other" @(Model.RecurringInterval == ReminderMonthInterval.Other ? "selected" : "")>@(Model.RecurringInterval == ReminderMonthInterval.Other && Model.CustomMonthInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMonthInterval} {Model.CustomMonthIntervalUnit}" : $"{translator.Translate(userLanguage, "Other")}") </!option>
<!option value="OneMonth" @(Model.RecurringInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage,"1 Month")</!option> <!option value="OneMonth" @(Model.RecurringInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage,"1 Month")</!option>
<!option value="ThreeMonths" @(Model.RecurringInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage, "3 Months")</!option> <!option value="ThreeMonths" @(Model.RecurringInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage, "3 Months")</!option>
<!option value="SixMonths" @(Model.RecurringInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage, "6 Months")</!option> <!option value="SixMonths" @(Model.RecurringInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage, "6 Months")</!option>
@@ -116,6 +109,7 @@
<script> <script>
var uploadedFiles = []; var uploadedFiles = [];
var customMonthInterval = @Model.CustomMonthInterval; var customMonthInterval = @Model.CustomMonthInterval;
var customMonthIntervalUnit = decodeHTMLEntities('@Model.CustomMonthIntervalUnit');
var recurringReminderRecordId = []; var recurringReminderRecordId = [];
getUploadedFilesFromModel(); getUploadedFilesFromModel();
function getUploadedFilesFromModel() { function getUploadedFilesFromModel() {

View File

@@ -54,25 +54,31 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Description" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -81,7 +87,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('TaxRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -111,6 +117,7 @@
<th scope="col" class="col-3 flex-grow-1 col-xl-1 text-truncate" data-column="date">@translator.Translate(userLanguage, "Date")</th> <th scope="col" class="col-3 flex-grow-1 col-xl-1 text-truncate" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-4 flex-grow-1 col-xl-6 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-4 flex-grow-1 col-xl-6 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('tax-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('tax-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -125,6 +132,7 @@
<td class="col-3 flex-grow-1 col-xl-1 text-truncate" data-column="date">@taxRecord.Date.ToShortDateString()</td> <td class="col-3 flex-grow-1 col-xl-1 text-truncate" data-column="date">@taxRecord.Date.ToShortDateString()</td>
<td class="col-4 flex-grow-1 col-xl-6 text-truncate" data-column="description">@taxRecord.Description</td> <td class="col-4 flex-grow-1 col-xl-6 text-truncate" data-column="description">@taxRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(taxRecord.Cost, hideZero))</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(taxRecord.Cost, hideZero))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", taxRecord.Files)</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(taxRecord.Notes)</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(taxRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -170,6 +178,9 @@
<li><hr class="context-menu-multiple dropdown-divider"></li> <li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'TaxRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
</ul> </ul>
@if (userColumnPreferences.Any()) @if (userColumnPreferences.Any())

View File

@@ -38,7 +38,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<a onclick="showRecurringReminderSelector('upgradeRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a> <a onclick="showRecurringReminderSelector('upgradeRecordDescription', 'upgradeRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div> </div>
</div> </div>
} }
@@ -52,14 +52,7 @@
<!option value="@tag">@tag</!option> <!option value="@tag">@tag</!option>
} }
</select> </select>
@foreach (ExtraField field in Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="upgradeRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="upgradeRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -54,31 +54,37 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li> <li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Date" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label> <label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Odometer" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label> <label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Description" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label> <label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Cost" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label> <label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div> </div>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='attachments' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Attachment">
<label class="form-check-label stretched-link" for="chkCol_Attachment">@translator.Translate(userLanguage, "Attachments")</label>
</div>
</li>
<li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Notes" checked> <input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label> <label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
@@ -87,7 +93,7 @@
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
var elementId = Guid.NewGuid(); var elementId = Guid.NewGuid();
<li class="dropdown-item"> <li class="dropdown-item" draggable="true" ondragstart="handleTableColumnDragStart(event)" ondragover="handleTableColumnDragOver(event)" ondragend="handleTableColumnDragEnd('UpgradeRecord')">
<div class="list-group-item"> <div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="@elementId"> <input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label> <label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
@@ -118,6 +124,7 @@
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 col-xl-4 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 col-xl-4 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th> <th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@translator.Translate(userLanguage, "Attachments")</th>
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th> <th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -133,6 +140,7 @@
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(upgradeRecord.Mileage == default ? "---" : upgradeRecord.Mileage.ToString())</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(upgradeRecord.Mileage == default ? "---" : upgradeRecord.Mileage.ToString())</td>
<td class="col-3 flex-grow-1 flex-shrink-1 col-xl-4 text-truncate" data-column="description">@upgradeRecord.Description</td> <td class="col-3 flex-grow-1 flex-shrink-1 col-xl-4 text-truncate" data-column="description">@upgradeRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(upgradeRecord.Cost, hideZero))</td> <td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@(StaticHelper.HideZeroCost(upgradeRecord.Cost, hideZero))</td>
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="attachments">@await Html.PartialAsync("_AttachmentColumn", upgradeRecord.Files)</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(upgradeRecord.Notes)</td> <td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields) @foreach (string extraFieldColumn in extraFields)
{ {
@@ -184,6 +192,8 @@
<li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="duplicateRecordsToOtherVehicles(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Duplicate To Vehicle")</span><i class="bi bi-copy"></i></div></a></li>
<li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li> <li><a class="dropdown-item" href="#" onclick="insertOdometer(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Create Odometer")</span><i class="bi bi-speedometer"></i></div></a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTabStickers(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Print")</span><i class="bi bi-printer"></i></div></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li> <li><a class="dropdown-item text-danger" href="#" onclick="deleteRecords(selectedRow, 'UpgradeRecord')"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Delete")</span><i class="bi bi-trash"></i></div></a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li> <li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li> <li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)"><div class="d-flex justify-content-between"><span class="me-5">@translator.Translate(userLanguage, "Statistics")</span><i class="bi bi-graph-up"></i></div></a></li>

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