Compare commits

...

292 Commits

Author SHA1 Message Date
Hargata Softworks
571e87dd14 Merge pull request #673 from hargata/Hargata/update.docs
Update screenshots and docs site.
2024-10-31 13:33:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
22eb38709a added show password button, updated configurator, fixed login page width. 2024-10-31 09:41:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
986a8420f8 Merge branch 'main' into Hargata/update.docs 2024-10-30 10:43:33 -06:00
Hargata Softworks
cff5f8e8e9 Merge pull request #679 from hargata/Hargata/518.2
Add ability to re-order tabs.
2024-10-30 09:36:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0df6cea88c backend for saving tab order 2024-10-30 09:33:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
88ff2432f6 Frontend for reordering tabs. 2024-10-29 23:18:31 -06:00
Hargata Softworks
efb7fb999b Merge pull request #677 from hargata/hargata-patch-1
Update screenshots.md
2024-10-28 20:48:35 -06:00
Hargata Softworks
1a2b503791 Update screenshots.md 2024-10-28 20:47:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e68d4ffa3a Merge branch 'main' into Hargata/update.docs 2024-10-28 19:21:50 -06:00
Hargata Softworks
d701a8964f Merge pull request #676 from hargata/Hargata/ios.resizebug
fix bug with iOS closing mobile nav when scrolling
2024-10-28 19:19:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b6929113bc fix bug with iOS closing mobile nav when scrolling 2024-10-28 19:18:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fc1eac1049 Merge branch 'main' into Hargata/update.docs 2024-10-28 15:58:56 -06:00
Hargata Softworks
189659ff1e Merge pull request #674 from hargata/Hargata/fix.wonky.columns
Fix bug where extra fields causes columns to be misaligned.
2024-10-28 15:58:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cb19130119 Fix bug where extra fields causes columns to be misaligned. 2024-10-28 15:52:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1596bf1deb Update screenshots and docs site. 2024-10-28 10:11:34 -06:00
Hargata Softworks
636cbb3d21 Merge pull request #672 from hargata/Hargata/translation.getter
further hardening.
2024-10-28 09:55:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cff7845dd4 further hardening. 2024-10-28 09:19:09 -06:00
Hargata Softworks
2d9cc3507f Merge pull request #671 from hargata/Hargata/translation.getter
Add functionality to download all translations.
2024-10-27 19:04:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4c6ac96dd1 Add functionality to download all translations. 2024-10-27 19:01:53 -06:00
Hargata Softworks
e5a594367e Merge pull request #670 from hargata/Hargata/translation.getter
Add translation getter
2024-10-27 09:54:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
12aaf8c209 spacing issue 2024-10-27 09:53:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cafbb156af Add translation getter 2024-10-27 09:52:21 -06:00
Hargata Softworks
36ac61d848 Merge pull request #669 from hargata/Hargata/translation.editor
fixed height for built in translator.
2024-10-25 16:56:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
df066a593e fixed height for built in translator. 2024-10-25 16:56:24 -06:00
Hargata Softworks
7ac9185e4c Merge pull request #668 from hargata/Hargata/translation.editor
Added translation editor.
2024-10-25 11:08:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
090cbba1a0 Added translation editor. 2024-10-25 11:06:40 -06:00
Hargata Softworks
5e3529f9cc Merge pull request #667 from hargata/Hargata/gps.timeout.fix
Fix distance accuracy
2024-10-24 22:35:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f90ddeabb4 Fix distance accuracy 2024-10-24 22:34:53 -06:00
Hargata Softworks
f54419b993 Merge pull request #666 from hargata/Hargata/gps.timeout.fix
set time out to 4 seconds and max age to 1 second to improve accuracy
2024-10-24 14:14:35 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cbbd7ff25f set time out to 4 seconds and max age to 1 second to improve accuracy 2024-10-24 14:13:47 -06:00
Hargata Softworks
0c2c089828 Merge pull request #664 from hargata/Hargata/485
hide identifier selector if no extra fields configured.
2024-10-23 11:08:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6a1813399f hide identifier selector if no extra fields configured. 2024-10-23 11:07:23 -06:00
Hargata Softworks
4c8d2b3704 Merge pull request #663 from hargata/Hargata/485
Allow users to select any extra field as vehicle identifier.
2024-10-23 10:54:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
23f97ebd46 Allow users to select any extra field as vehicle identifier. 2024-10-23 10:52:41 -06:00
Hargata Softworks
e3dccf0182 Merge pull request #662 from hargata/Hargata/gps.integration
further hardening
2024-10-22 11:32:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
272596878c further hardening 2024-10-22 11:25:48 -06:00
Hargata Softworks
46953e9063 Merge pull request #659 from hargata/Hargata/gps.integration
further bug fixes and hardening.
2024-10-21 10:53:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
30f35855f2 further bug fixes and hardening. 2024-10-21 10:53:09 -06:00
Hargata Softworks
df94132917 Merge pull request #658 from hargata/Hargata/gps.integration
Hargata/gps.integration
2024-10-20 20:59:03 -06:00
DESKTOP-T0O5CDB\DESK-555BD
de37d689be moved variable to partial view. 2024-10-20 20:51:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4f941aad52 add functionality to show sub odometer readings. 2024-10-20 20:41:43 -06:00
Hargata Softworks
2febdf1259 Merge pull request #657 from hargata/Hargata/gps.integration
fixes for GPS integration and experimental feature banner.
2024-10-20 19:12:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
349808a609 fixes for GPS integration and experimental feature banner. 2024-10-20 19:10:23 -06:00
Hargata Softworks
7d58d1bc40 Merge pull request #656 from hargata/Hargata/gps.integration
GPS methods to track vehicle odometer. experimental feature.
2024-10-20 15:31:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5e91a26230 GPS methods to track vehicle odometer. experimental feature. 2024-10-20 15:30:22 -06:00
Hargata Softworks
0942ae1742 Merge pull request #655 from hargata/Hargata/chore.update.deps
CHORE: Update Dependencies
2024-10-18 09:42:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cce9bd29e5 z 2024-10-18 09:42:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
05fdeef862 CHORE: Update Dependencies 2024-10-18 09:41:44 -06:00
Hargata Softworks
dfa08b287c Merge pull request #654 from hargata/Hargata/fix.tabs
improve tabs appearance
2024-10-17 15:06:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
77920f94cd improve tabs appearance 2024-10-17 15:05:20 -06:00
Hargata Softworks
3631a036c6 Merge pull request #653 from hargata/Hargata/616
thumbnail resize using Hermite interpolation
2024-10-17 10:37:54 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1445959d05 thumbnail resize using Hermite interpolation 2024-10-17 10:34:00 -06:00
Hargata Softworks
0b45de5ba8 Merge pull request #652 from hargata/Hargata/fix.titles
Fixed the Titles.
2024-10-16 15:25:44 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d86cdfcefd Fixed the Titles. 2024-10-16 14:25:19 -06:00
Hargata Softworks
0c7aa304f7 Create CONTRIBUTING.md 2024-10-16 12:36:38 -06:00
Hargata Softworks
e5114f491d Merge pull request #651 from hargata/Hargata/fix.adjustedodomete
1.3.9 Changes
2024-10-15 12:43:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e238bd8a12 translation keys 2024-10-15 11:50:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f53ad74c72 fix bugs with alternate fuel units when the average min and max labels don't exist. 2024-10-14 21:37:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3d456d1cd5 quick styling fix 2024-10-14 10:16:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
de32fe4d85 load sponsors in ajax to account for users with slower internet connection. 2024-10-14 09:42:26 -06:00
DESKTOP-T0O5CDB\DESK-555BD
29a90fe814 add note records to attachment export. 2024-10-13 12:25:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3f31f091dc fixed bug with UK units. 2024-10-10 15:41:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
571b558181 allow extra fields edit for multiple odometer records. 2024-10-10 09:38:04 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ed0775ab71 Display fuel mileage unit and fix bug with metric fuel mileages 2024-10-10 10:08:34 -06:00
DESKTOP-T0O5CDB\DESK-555BD
19e19d7c15 extra field edit for multiple gas records. 2024-10-09 22:23:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b7e574889c Allow Extra Fields to be edited for multiple records for Service, Repair, and Upgrade Records. 2024-10-09 21:29:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2a66c97254 separated out the import and export methods. 2024-10-09 16:02:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e14ef961b9 improved maintainability by splitting massive controller down to sections. 2024-10-09 15:58:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec2c17be23 updated configurator and streamlined getAndValidateExtraFields method. 2024-10-09 09:13:07 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b5162d2044 configurator webpage 2024-10-05 20:04:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ae428b94d1 Update ENV file to direct users to configurator 2024-10-05 20:04:07 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3350f3c39a confirm deletion. 2024-10-05 18:40:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
68caf82167 moved token into modal. 2024-10-05 18:02:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8c5f1c3a01 fixed minor bug issue in admin panel page and allow for multiple email addresses to generate token for 2024-10-05 17:28:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e1bbc232e1 removed icon. 2024-10-05 16:12:48 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fbc7a39996 Merge branch 'Hargata/admin.panel.enhancement' into Hargata/fix.adjustedodomete 2024-10-05 16:03:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1ce77ad1ee create app schema if not exists without relying on init.sql 2024-10-05 12:03:46 -06:00
DESKTOP-T0O5CDB\DESK-555BD
08cee8029f more ctrl enter forms. 2024-09-29 19:44:28 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e33bcf5e57 re-align some settings switches 2024-09-29 15:25:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cf5ceaa959 Ctrl and Enter to submit record forms 2024-09-29 15:13:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8293bed89c added warning for when user is running LubeLogger without a preconfigured culture. 2024-09-29 10:58:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a830580c27 moved delete vehicle button to edit vehicle modal. 2024-09-29 10:31:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
94c6cca25c add enter event on sweetalert modals see #639 2024-09-28 12:44:09 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78e0a0146c chore: fix deprecated click function on DOM elements 2024-09-28 12:05:48 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ea667ed950 visual enhancements for admin panel 2024-09-28 10:44:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f8b23b5f20 Fix the adjustedodometer endpoint for decimal odometer multiplier 2024-09-25 21:02:49 -06:00
Hargata Softworks
b69749b073 Merge pull request #635 from hargata/Hargata/update.demo
Update demo defaults.
2024-09-24 22:35:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
951eea844f Updated demo default 2024-09-24 22:35:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3e5aff0c9 Updated demo file 2024-09-24 22:33:00 -06:00
Hargata Softworks
ef3e450e4f Merge pull request #634 from hargata/Hargata/api.enhancement.dos
Added documentation and new API endpoint to calculate adjusted odometer.
2024-09-24 22:05:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8272fa20da Added documentation and new API endpoint to calculate adjusted odometer. 2024-09-24 22:04:31 -06:00
Hargata Softworks
9ef0a748a3 Merge pull request #633 from hargata/Hargata/563.619
added flag for optional odometer.
2024-09-24 19:20:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6de928aef4 added flag for optional odometer. 2024-09-24 19:18:13 -06:00
Hargata Softworks
34ff874949 Merge pull request #632 from hargata/Hargata/563.619
make odometer optional
2024-09-24 18:43:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a653f3ac7f make odometer optional 2024-09-24 18:36:59 -06:00
Hargata Softworks
dc96084406 Merge pull request #631 from hargata/Hargata/630
Fix light mode bug.
2024-09-24 18:30:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e056aaa7dc Fix light mode bug. 2024-09-24 18:11:50 -06:00
Hargata Softworks
73bb3c11b4 Merge pull request #628 from hargata/Hargata/adaptive.color.mode
fix label.
2024-09-23 08:37:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c88a040728 fix label. 2024-09-23 08:36:58 -06:00
Hargata Softworks
dfa56c2b12 Merge pull request #627 from hargata/Hargata/adaptive.color.mode
Added switch for adaptive color mode.
2024-09-23 08:34:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
104d4bab52 Added switch for adaptive color mode. 2024-09-23 08:31:54 -06:00
Hargata Softworks
6916161e87 Merge pull request #626 from hargata/Hargata/info.endpoint.update
#482 - Allow Root Users to Login via OIDC
2024-09-22 14:36:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
512852d217 added flag to enable root user to login via OIDC 2024-09-22 14:32:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a61c699417 Added PlanRecords to info endpoint 2024-09-22 13:48:01 -06:00
Hargata Softworks
1e832f014f Merge pull request #625 from hargata/Hargata/update.links
Updated documentation links and removed static documentation.
2024-09-22 10:42:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e9a463e2e5 Updated documentation links and removed static documentation. 2024-09-22 10:41:36 -06:00
Hargata Softworks
d0c19d0436 Merge pull request #624 from hargata/Hargata/618
#618
2024-09-22 10:24:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3428abf358 code clean up for default reminder email and added flag to disable registration on front end. 2024-09-22 10:20:25 -06:00
Hargata Softworks
e6857331e2 Merge pull request #617 from hargata/Hargata/603
adaptive color mode if dark mode is not enabled.
2024-09-13 14:24:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1fd93d2efe update variable name 2024-09-13 14:24:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84e44acbfe adaptive color mode if dark mode is not enabled. 2024-09-13 07:51:32 -06:00
Hargata Softworks
77014c71f2 Merge pull request #614 from hargata/Hargata/517
Add Default Reminder Email Recipient
2024-09-12 11:15:55 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2db01aa4ad reworded response message 2024-09-12 10:00:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6c3fa21cc5 Added environment variable to configure default reminder email recipient. Fixed bug with due date in email body and improved logging and error catching when sending emails. 2024-09-12 09:43:52 -06:00
Hargata Softworks
26b7d101ab Merge pull request #610 from hargata/Hargata/609
Added custom thresholds for reminder.
2024-09-07 13:37:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3c3cdf0cb Added custom thresholds for reminder. 2024-09-07 08:39:26 -06:00
Hargata Softworks
e62fa31485 Merge pull request #608 from hargata/Hargata/606
See #606
2024-09-05 13:19:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5f283c1558 uh 2024-09-05 13:11:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
52163098d2 See #606 2024-09-05 13:04:16 -06:00
Hargata Softworks
dfe27f0577 Merge pull request #605 from hargata/Hargata/598
Added colored icons for Planner items.
2024-09-04 09:23:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e415a3468f Added colored icons for Planner items. 2024-09-03 20:13:07 -06:00
Hargata Softworks
71302a52e4 Merge pull request #602 from hargata/Hargata/light.mode.menu.fix
Fix menu color when in light mode.
2024-09-02 11:41:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
762730eb0f Fix menu color when in light mode. 2024-09-02 11:40:11 -06:00
Hargata Softworks
3999a8a54d Merge pull request #601 from hargata/Hargata/div.zero.hotfix
Fix divide by zero error in Cost Per Mile metric
2024-09-01 11:02:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
065291e146 Fix divide by zero error in Cost Per Mile metric 2024-09-01 11:02:04 -06:00
Hargata Softworks
99376aba1c Merge pull request #600 from hargata/Hargata/537
Add user-configurable dashboard metric
2024-09-01 10:48:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
698e8a0a95 fix default placeholder label for data table. 2024-08-31 23:50:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b1b3a127dc fix bug with data table. 2024-08-31 20:28:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec55c95c71 Allow users to set which dashboard metric they want to see in the garage. 2024-08-31 20:28:32 -06:00
Hargata Softworks
e6eb2b473c Merge pull request #599 from hargata/Hargata/551
Added data table
2024-08-31 17:36:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a4b39820e4 updated label. 2024-08-31 10:39:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4a20c81047 convert to number of days and replace zero values with dashes 2024-08-31 10:33:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b72ab461e5 revert and enhance. 2024-08-31 09:46:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0be498d9bd Added data table 2024-08-30 15:29:09 -06:00
Hargata Softworks
00bad986e5 Merge pull request #596 from hargata/Hargata/reminder.tooltip.fix
Stats API Endpoint
2024-08-29 08:19:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4710e84dcf more stats. 2024-08-27 12:20:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ee55f8c884 added vehicle info API endpoint for dashboards and whatnot. 2024-08-27 12:04:57 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a74116e70 Do not load tooltips on touch screen only device. 2024-08-27 11:07:59 -06:00
Hargata Softworks
cfd83b5b4e Merge pull request #594 from hargata/Hargata/588
removed order by
2024-08-26 15:25:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e468261095 removed order by 2024-08-26 15:23:20 -06:00
Hargata Softworks
1362dc87df Merge pull request #593 from hargata/Hargata/588
Allow extra fields to be added via API
2024-08-26 15:13:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b89902bbdb Allow extra fields to be added via API 2024-08-26 15:12:05 -06:00
Hargata Softworks
f31b70a6dc Merge pull request #592 from hargata/Hargata/531
Display the due date and odometer in a tooltip in the reminder record…
2024-08-26 12:43:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
47a1f0c4d5 Display the due date and odometer in a tooltip in the reminder records view. 2024-08-26 12:38:42 -06:00
Hargata Softworks
3234b530d5 Merge pull request #591 from hargata/Hargata/reminder.api.enhancement
Add due date and due odometer to reminder output for API method.
2024-08-26 10:33:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
75d16544db Add due date and due odometer to reminder output for API method. 2024-08-26 10:32:14 -06:00
Hargata Softworks
7179b60116 Merge pull request #590 from hargata/Hargata/csv.extrafield.export
remove unused variable.
2024-08-23 13:45:22 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7cb5d8254f remove unused variable. 2024-08-23 13:44:58 -06:00
Hargata Softworks
6ab7ee6e4f Merge pull request #589 from hargata/Hargata/csv.extrafield.export
Add functionality to export extra fields in CSV exports.
2024-08-23 13:41:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a57ce55f8b Fix type 2024-08-23 13:41:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
190484762d Add functionality to export extra fields in CSV exports. 2024-08-23 13:39:08 -06:00
Hargata Softworks
78554ade5d Merge pull request #587 from hargata/Hargata/api.enhancement
Further Enhance Cleanup API Endpoint.
2024-08-22 11:13:04 -06:00
DESKTOP-T0O5CDB\DESK-555BD
364a13226a Added return object. 2024-08-22 11:11:17 -06:00
Hargata Softworks
691813838c Merge pull request #586 from hargata/Hargata/cleanup.api
Add API Controller method to clean up temp files and unlinked thumbnails
2024-08-21 21:24:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0defb0fadf json json. 2024-08-21 21:23:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
08bf00ee37 Add API Controller method to clean up temp files and unlinked thumbnails. 2024-08-21 21:21:24 -06:00
Hargata Softworks
522322ee2a Merge pull request #585 from hargata/Hargata/570
Upload files on paste.
2024-08-21 18:13:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
062e3600e7 update uploaded files in real time for records that already have uploaded files and fix note attachment bug 2024-08-21 18:11:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a92160f6d7 Upload files on paste. 2024-08-21 14:38:15 -06:00
Hargata Softworks
4adf967f7f Merge pull request #584 from hargata/Hargata/534
Add fuel type dropdown and diesel fuel type.
2024-08-19 12:13:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fd6bd98a25 Missing translation key. 2024-08-19 12:12:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fe76f32778 Add fuel type dropdown and diesel fuel type. 2024-08-19 12:09:50 -06:00
Hargata Softworks
897b4f2efe Merge pull request #581 from hargata/Hargata/576
Resolve Empty Tables Postgres Bug.
2024-08-14 09:27:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
267f903ffe Resolve bug where attempting to delete a vehicle with zero records returns an error. 2024-08-13 08:19:05 -06:00
Hargata Softworks
db884d0a6a Merge pull request #569 from hargata/Hargata/568
Add PKCE to OIDC Auth Flow
2024-07-31 12:28:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d1190e7ddb Update readme to add helm chart by anza labs. 2024-07-31 12:27:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dae8ab679e add better logging for when IdP returns errors. 2024-07-30 11:57:35 -06:00
DESKTOP-T0O5CDB\DESK-555BD
86ec9409c3 added pkce code for when user logins automatically. 2024-07-29 15:28:00 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2c4ddb0c38 Updated version. 2024-07-29 15:23:31 -06:00
DESKTOP-T0O5CDB\DESK-555BD
44d10f11ca Add PKCE functionality(RFC 7636) to OIDC functionality. 2024-07-29 15:23:10 -06:00
Hargata Softworks
6cf733b9c6 Merge pull request #558 from hargata/Hargata/545
enable extra fields to be imported via CSV
2024-07-09 14:52:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1eb6e2cedf enable extra fields to be imported via CSV 2024-07-09 14:48:09 -06:00
Hargata Softworks
ef4deaba8f Merge pull request #557 from hargata/Hargata/512
recurring interval are now based on reminder metric.
2024-07-09 11:16:09 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8c196c2fa recurring interval are now based on reminder metric. 2024-07-09 11:15:02 -06:00
Hargata Softworks
f7c9db6353 Merge pull request #556 from hargata/Hargata/mailconfig.cleanup
Clean up MailConfig and update npsql
2024-07-09 10:35:16 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4ce720ff97 Update npgSQL 2024-07-09 10:34:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a96011629b clean up mailconfig 2024-07-09 10:32:40 -06:00
Hargata Softworks
9dcdcf97e8 Merge pull request #508 from snpaul22/main
Create app schema automatically
2024-07-06 17:40:13 -06:00
snpaul22
5148338f52 Merge branch 'hargata:main' into main 2024-07-06 18:54:14 -04:00
Hargata Softworks
0d3c04d8f8 Merge pull request #554 from hargata/Hargata/root.user.update
Make it easier to update root user credentials and patch bug.
2024-07-05 11:26:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
15328a14b4 Make it easier to update root user credentials and patch bug. 2024-07-05 11:18:46 -06:00
Hargata Softworks
2d092f722a Merge pull request #550 from hargata/Hargata/disabled.init.odo
updated button color.
2024-07-02 15:05:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8825cb9b9b updated button color. 2024-07-02 15:05:16 -06:00
Hargata Softworks
2f17e303ab Merge pull request #549 from hargata/Hargata/disabled.init.odo
Disable the initial odometer reading field
2024-07-02 11:51:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4b56c8a343 a better implementation of disabled attribute 2024-07-02 11:09:41 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7b6b62c623 Disable the initial odometer reading field if it's non-zero to prevent accidental editing. 2024-07-02 10:54:11 -06:00
Hargata Softworks
07f5e66491 Merge pull request #519 from NateWright/main
Add maskable icons for Android PWA
2024-06-18 08:25:49 -06:00
Hargata Softworks
fe633f3220 Merge pull request #542 from kapcake/patch-1
Update money regex to allow more than 6 figures on integer part
2024-06-18 08:25:00 -06:00
kapcake
af1090553f Update money regex to not allow unlimited figures
The regex now allows up to 8 groups of 3 digits on the integer part, which is within the bounds of C# decimal, preventing the user to go out of bounds on the form input.
2024-06-18 15:14:26 +02:00
kapcake
92c2e66660 Update money regex to allow more than 6 figures on integer part 2024-06-18 15:02:20 +02:00
Nathan Wright
08372f9dcb Merge branch 'hargata:main' into main 2024-06-08 15:20:31 -04:00
Hargata Softworks
64ea0e2eee Merge pull request #533 from hargata/Hargata/532
add support for smtp clients that requires no authentication.
2024-05-31 09:11:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
7ab476a88f add support for smtp clients that requires no authentication. 2024-05-31 09:11:09 -06:00
Hargata Softworks
de41ca911d Merge pull request #530 from hargata/Hargata/527
Minor Bug Fixes
2024-05-29 09:36:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dde9688f96 Updated supplies usage validation to allow for free supplies. 2024-05-28 14:54:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c1ca63edc0 removed unnecessary tostring 2024-05-28 14:26:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cbc430499f added new currency formatting function 2024-05-28 14:14:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4da9fa4802 See 514 2024-05-28 13:55:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
42586d9556 Updated version number 2024-05-28 12:56:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ea4387d4ab Add missing translation keys 2024-05-28 12:53:20 -06:00
Hargata Softworks
0707b515ab Merge pull request #525 from hargata/Hargata/sponsor.tiers
Updated Sponsors File Path
2024-05-21 15:42:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78ae71fc46 Updated Sponsors File Path 2024-05-21 15:41:05 -06:00
Hargata Softworks
3f62cd40e7 Merge pull request #524 from hargata/Hargata/sponsor.tiers
Add Sponsor Section
2024-05-21 15:32:20 -06:00
DESKTOP-T0O5CDB\DESK-555BD
47657c0093 Added sponsor section. 2024-05-21 15:29:46 -06:00
Hargata Softworks
a5b0fde4b6 Merge pull request #522 from hargata/Hargata/swal.uncensored
Hargata/swal.uncensored
2024-05-20 12:26:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
78cc0b34b1 Updated version number 2024-05-20 12:25:07 -06:00
Hargata Softworks
e3017e986b Merge pull request #520 from hargata/Hargata/license.change
Removed commercial license restrictions from license.
2024-05-20 12:22:21 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e12cd876db removed unused files 2024-05-20 12:21:54 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5292e4b814 utilize uncensored version of sweet alert 2024-05-20 12:16:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
dbdd16ab89 Removed commercial license restrictions from license. 2024-05-14 08:46:47 -06:00
nwright
61c2600286 added maskable icons 2024-05-13 19:34:03 -04:00
snpaul22
163a33ae3a Create init.sql
Script checks for "app" schema in Postgres during startup. It will create the schema automatically if it doesn't exist or skip if it does exist.
2024-05-04 11:27:50 -04:00
snpaul22
37d064aa62 Update docker-compose.postgresql.yml
Added Postgres volume bind for initialization script
2024-05-04 11:23:55 -04:00
Hargata Softworks
ddc3c2e1b5 Merge pull request #503 from hargata/Hargata/null.mail.config
null check for mail config
2024-04-25 12:45:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
49184b287b null check for mail config 2024-04-25 12:45:21 -06:00
Hargata Softworks
f6139bda0d Merge pull request #502 from hargata/Hargata/mailkit.upgrade
fix inefficiencies
2024-04-25 11:33:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a6471b823b fix inefficiencies 2024-04-25 11:11:54 -06:00
Hargata Softworks
7c34003647 Merge pull request #501 from hargata/Hargata/mailkit.upgrade
MailKit Upgrade
2024-04-25 10:46:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1aa21f9980 Uprgade from .NET SMTPClient to MailKit as the default smtpclient does not support modern protocols. 2024-04-25 10:45:55 -06:00
Hargata Softworks
ce4ca50939 Merge pull request #499 from hargata/Hargata/persist.metric.bug
fix getyear method
2024-04-23 23:50:31 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fb28260c4a fix getyear method 2024-04-23 23:50:06 -06:00
Hargata Softworks
626a904747 Merge pull request #498 from hargata/Hargata/persist.metric.bug
Minor bug fix
2024-04-23 23:43:18 -06:00
DESKTOP-T0O5CDB\DESK-555BD
893cdafdc5 Fixes a very minor bug where the persisted year metric blanks out when viewing a different car that doesn't have that specific year. 2024-04-23 23:42:09 -06:00
Hargata Softworks
dbfb7d7d9c Merge pull request #493 from hargata/Hargata/persist.dashboard
check against null instead of undefined,
2024-04-15 08:42:43 -06:00
DESKTOP-GENO133\IvanPlex
a66538a7db check against null instead of undefined, 2024-04-15 08:41:53 -06:00
Hargata Softworks
2f77d87d4f Merge pull request #492 from hargata/Hargata/persist.dashboard
temporarily persist dashboard metrics in sessionStorage
2024-04-15 08:25:54 -06:00
DESKTOP-GENO133\IvanPlex
de85ba984c temporarily persist dashboard metrics in sessionStorage 2024-04-15 08:20:37 -06:00
Hargata Softworks
caac1a05ae Merge pull request #480 from hargata/Hargata/fix.link.color
fix donation link colors
2024-04-12 08:04:44 -06:00
Hargata Softworks
eb5793b819 Merge pull request #490 from hargata/Hargata/fix.alt.fuel
Fix Alternate Fuel Mileage Bug
2024-04-12 08:04:28 -06:00
DESKTOP-GENO133\IvanPlex
5ef3e1e2ce updated version number 2024-04-12 08:04:04 -06:00
DESKTOP-GENO133\IvanPlex
d8b459e5ee persist regional formatting when viewing record statistics. 2024-04-12 08:01:25 -06:00
DESKTOP-GENO133\IvanPlex
7b40d58aa1 fix alternate fuel mileage auto converting to NA decimal format bug. 2024-04-12 07:54:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
809e9b838e fix donation link colors 2024-04-09 21:29:19 -06:00
Hargata Softworks
23ae36ebd9 Merge pull request #475 from hargata/Hargata/update.readme
check if function exists.
2024-04-07 21:03:31 -06:00
DESKTOP-GENO133\IvanPlex
9c3f7d20f5 fix plans not updating when deleted. 2024-04-07 21:01:06 -06:00
DESKTOP-GENO133\IvanPlex
083298303c check if function exists. 2024-04-07 20:51:55 -06:00
Hargata Softworks
224970a07e Merge pull request #474 from hargata/Hargata/update.readme
Fix the problem with time.
2024-04-07 19:15:41 -06:00
DESKTOP-GENO133\IvanPlex
86d039e5b0 Fix the problem with time. 2024-04-07 17:59:08 -06:00
Hargata Softworks
6a8038aac9 Merge pull request #473 from hargata/Hargata/update.readme
update readme
2024-04-07 13:59:02 -06:00
DESKTOP-GENO133\IvanPlex
e748f08a8e update readme 2024-04-07 13:58:32 -06:00
Hargata Softworks
3580963e9f Merge pull request #472 from hargata/Hargata/planner.improvement.part2
Improved UI UX For Planner Edit
2024-04-07 12:51:28 -06:00
DESKTOP-GENO133\IvanPlex
b008ce2ab8 Improved UI UX 2024-04-07 12:50:28 -06:00
Hargata Softworks
ea0c2c7061 Merge pull request #471 from hargata/Hargata/transl
Updated translation
2024-04-07 11:26:35 -06:00
DESKTOP-GENO133\IvanPlex
0926220933 Updated translation 2024-04-07 11:26:12 -06:00
Hargata Softworks
ca975bbdd3 Merge pull request #470 from hargata/Hargata/planner.template.edit
updated translation
2024-04-07 11:22:20 -06:00
DESKTOP-GENO133\IvanPlex
b16c5c5302 updated translation 2024-04-07 11:21:43 -06:00
Hargata Softworks
cad05fe5d9 Merge pull request #466 from hargata/Hargata/planner.improvement
Moved template modal outside of add modal.
2024-04-07 11:20:42 -06:00
Hargata Softworks
a7cd466d9c Merge pull request #469 from hargata/Hargata/planner.template.edit
Add functionality to let users edit plan record templates
2024-04-07 11:20:28 -06:00
DESKTOP-GENO133\IvanPlex
4472a67ec0 fixed label function 2024-04-07 11:19:52 -06:00
DESKTOP-GENO133\IvanPlex
c84a4029ec basic functionality to edit templates 2024-04-07 11:09:05 -06:00
DESKTOP-GENO133\IvanPlex
d7d9ab505e Add functionality to let users edit plan record templates 2024-04-07 08:31:17 -06:00
DESKTOP-GENO133\IvanPlex
c582f5f5c7 Moved template modal outside of add modal. 2024-04-05 19:36:26 -06:00
Hargata Softworks
4c30939339 Merge pull request #465 from hargata/Hargata/unused.translation.key
Updated translation file and unsaved changes label color.
2024-04-05 18:50:02 -06:00
DESKTOP-GENO133\IvanPlex
cecd6a1d2b Updated translation file and unsaved changes label color. 2024-04-05 18:48:54 -06:00
Hargata Softworks
853dcbb364 Merge pull request #464 from hargata/Hargata/unused.translation.key
removed cached translation key
2024-04-05 07:18:08 -06:00
DESKTOP-GENO133\IvanPlex
ae327ed26d removed cached translation key 2024-04-05 07:17:49 -06:00
DESKTOP-GENO133\IvanPlex
3e416aa255 Revert "removed unused translation key"
This reverts commit ab98d02106faceaca1c6c76b8f0de7bf3e28cebe.
2024-04-05 07:16:11 -06:00
DESKTOP-GENO133\IvanPlex
61c2c3fc83 removed unused translation key 2024-04-05 07:16:10 -06:00
Hargata Softworks
ca749aaf1e Merge pull request #463 from hargata/Hargata/unsaved.changes
updated cached view to only show when there are unsaved changes present.
2024-04-05 07:11:50 -06:00
DESKTOP-GENO133\IvanPlex
2a2cb3bd0c updated cached view to only show when there are unsaved changes present. 2024-04-05 07:10:09 -06:00
Hargata Softworks
44e3d19844 Merge pull request #462 from hargata/Hargata/global.search
added incremental search
2024-04-04 19:16:22 -06:00
DESKTOP-GENO133\IvanPlex
1e25fffc70 added incremental search 2024-04-04 19:14:49 -06:00
Hargata Softworks
44645ed23d Merge pull request #461 from hargata/Hargata/global.search
added global search
2024-04-04 10:48:57 -06:00
DESKTOP-GENO133\IvanPlex
fa557e5f76 added global search 2024-04-04 10:46:43 -06:00
Hargata Softworks
7202fda38e Merge pull request #458 from hargata/Hargata/cached.records
Cached Record.
2024-04-02 21:53:46 -06:00
DESKTOP-GENO133\IvanPlex
d05afe41d6 Cache the record the user was viewing if they didn't save, delete, or move it. 2024-04-02 21:51:02 -06:00
Hargata Softworks
bfc0b58728 Merge pull request #453 from hargata/Hargata/bring.back.modal
Allow users to bring back modal window they accidentally closed.
2024-04-02 07:39:25 -06:00
DESKTOP-GENO133\IvanPlex
22e8aaca81 Allow users to bring back modal window they accidentally closed. 2024-04-02 07:29:16 -06:00
Hargata Softworks
5cb1247fb2 Merge pull request #452 from hargata/Hargata/odometer.increment
Odometer Increments
2024-04-01 18:31:27 -06:00
DESKTOP-GENO133\IvanPlex
17a6d99703 Allow users to increment for last reported odometer reading when creating new records. 2024-04-01 18:25:20 -06:00
Hargata Softworks
3e7917f767 Create FUNDING.yml 2024-04-01 09:14:46 -06:00
Hargata Softworks
e9277d4dd9 Merge pull request #443 from hargata/Hargata/reminder.icons
reminders will now display urgency in icons instead of a full badge w…
2024-03-29 09:35:17 -06:00
DESKTOP-GENO133\IvanPlex
058edd8af6 reminders will now display urgency in icons instead of a full badge when viewed in small screens. 2024-03-29 08:15:55 -06:00
Hargata Softworks
8ed7dcb9ff Merge pull request #441 from hargata/Hargata/reminder.mobile
Improved Reminder page usability on mobile
2024-03-28 19:15:49 -06:00
DESKTOP-GENO133\IvanPlex
1f4827abc0 Improved Reminder page usabilty on mobile 2024-03-28 19:13:19 -06:00
Hargata Softworks
9715a0fcf7 Merge pull request #440 from hargata/Hargata/vehicle.garage.tags
Added status tags
2024-03-27 20:22:44 -06:00
DESKTOP-GENO133\IvanPlex
9bf475c352 Added status tags 2024-03-27 20:16:56 -06:00
Hargata Softworks
c2aeb4bca0 Merge pull request #438 from hargata/Hargata/garage.status
Hargata/garage.status
2024-03-27 11:41:43 -06:00
DESKTOP-GENO133\IvanPlex
07b3020999 use current mileage and date when renewing reminders. 2024-03-27 11:39:30 -06:00
DESKTOP-GENO133\IvanPlex
c2a7f39025 add depreciation calculation 2024-03-27 09:24:33 -06:00
DESKTOP-GENO133\IvanPlex
a92d422972 add purchase and sold price 2024-03-27 08:21:02 -06:00
Hargata Softworks
345eb65c3a Merge pull request #437 from hargata/Hargata/copy.supplies.attachments
Option to copy supplies attachments into records.
2024-03-26 18:33:15 -06:00
DESKTOP-GENO133\IvanPlex
a459973983 Add functionality to copy over attachments from supplies to records that are utilizing them. 2024-03-26 18:23:16 -06:00
Hargata Softworks
470dd4d78a Merge pull request #435 from hargata/Hargata/reminder.tag
Reminder Tags
2024-03-26 08:47:14 -06:00
DESKTOP-GENO133\IvanPlex
e4cb183140 improve readability on urgent reminder labels, filtering by tag now updates aggregate counts. 2024-03-26 08:40:30 -06:00
DESKTOP-GENO133\IvanPlex
6455af96bf Add tag functionality to reminders. 2024-03-26 08:11:14 -06:00
Hargata Softworks
42afa87464 Merge pull request #432 from hargata/Hargata/multiple.reminders
added translation layer
2024-03-25 06:46:59 -06:00
DESKTOP-GENO133\IvanPlex
97466eeff2 added translation layer 2024-03-25 06:45:15 -06:00
Hargata Softworks
bcbfd4ba9c Merge pull request #426 from hargata/Hargata/multiple.reminders
Select Multiple Reminders when creating new record.
2024-03-23 20:55:04 -06:00
DESKTOP-GENO133\IvanPlex
cfa052fc31 Fix styling 2024-03-23 20:52:46 -06:00
DESKTOP-GENO133\IvanPlex
3f71f6a8d8 Updated version number. 2024-03-23 20:35:41 -06:00
DESKTOP-GENO133\IvanPlex
b70e442ca3 Allow users to select multiple reminders to push back when creating new record. 2024-03-23 20:32:23 -06:00
200 changed files with 8141 additions and 14271 deletions

7
.env
View File

@@ -2,12 +2,11 @@ LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8
MailConfig__EmailServer=""
MailConfig__EmailFrom=""
MailConfig__UseSSL="false"
MailConfig__Port=587
MailConfig__Username=""
MailConfig__Password=""
LOGGING__LOGLEVEL__DEFAULT=Error
# * Uncoment this line if you use postgresSQL as database backend.
# * Check the docker-compose.postgresql.yml file
#POSTGRES_CONNECTION="Host=postgres;Username=lubelogger;Password=lubepass;Database=lubelogger;"
# This file is provided as a GUIDELINE ONLY
# Use the LubeLogger Configurator to configure your environment variables
# https://lubelogger.com/configure

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -21,11 +21,15 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
private readonly IPlanRecordDataAccess _planRecordDataAccess;
private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess;
private readonly IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic;
private readonly IVehicleLogic _vehicleLogic;
private readonly IOdometerLogic _odometerLogic;
private readonly IFileHelper _fileHelper;
private readonly IMailHelper _mailHelper;
@@ -41,12 +45,16 @@ namespace CarCareTracker.Controllers
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
ISupplyRecordDataAccess supplyRecordDataAccess,
IPlanRecordDataAccess planRecordDataAccess,
IPlanRecordTemplateDataAccess planRecordTemplateDataAccess,
IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper,
IFileHelper fileHelper,
IConfigHelper config,
IUserLogic userLogic,
IVehicleLogic vehicleLogic,
IOdometerLogic odometerLogic)
{
_dataAccess = dataAccess;
@@ -58,6 +66,9 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_supplyRecordDataAccess = supplyRecordDataAccess;
_planRecordDataAccess = planRecordDataAccess;
_planRecordTemplateDataAccess = planRecordTemplateDataAccess;
_userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper;
@@ -65,6 +76,7 @@ namespace CarCareTracker.Controllers
_reminderHelper = reminderHelper;
_userLogic = userLogic;
_odometerLogic = odometerLogic;
_vehicleLogic = vehicleLogic;
_fileHelper = fileHelper;
_config = config;
}
@@ -87,19 +99,119 @@ namespace CarCareTracker.Controllers
}
return Json(result);
}
[HttpGet]
[Route("/api/vehicle/info")]
public IActionResult VehicleInfo(int vehicleId)
{
//stats for a specific or all vehicles
List<Vehicle> vehicles = new List<Vehicle>();
if (vehicleId != default)
{
if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleId))
{
vehicles.Add(_dataAccess.GetVehicleById(vehicleId));
} else
{
return new RedirectResult("/Error/Unauthorized");
}
} else
{
var result = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
result = _userLogic.FilterUserVehicles(result, GetUserID());
}
vehicles.AddRange(result);
}
List<VehicleInfo> apiResult = new List<VehicleInfo>();
foreach(Vehicle vehicle in vehicles)
{
var currentMileage = _vehicleLogic.GetMaxMileage(vehicle.Id);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id);
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id);
var planRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id);
var resultToAdd = new VehicleInfo()
{
VehicleData = vehicle,
LastReportedOdometer = currentMileage,
ServiceRecordCount = serviceRecords.Count(),
ServiceRecordCost = serviceRecords.Sum(x=>x.Cost),
RepairRecordCount = repairRecords.Count(),
RepairRecordCost = repairRecords.Sum(x=>x.Cost),
UpgradeRecordCount = upgradeRecords.Count(),
UpgradeRecordCost = upgradeRecords.Sum(x=>x.Cost),
GasRecordCount = gasRecords.Count(),
GasRecordCost = gasRecords.Sum(x=>x.Cost),
TaxRecordCount = taxRecords.Count(),
TaxRecordCost = taxRecords.Sum(x=> x.Cost),
VeryUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.VeryUrgent),
PastDueReminderCount = results.Count(x => x.Urgency == ReminderUrgency.PastDue),
UrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.Urgent),
NotUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.NotUrgent),
PlanRecordBackLogCount = planRecords.Count(x=>x.Progress == PlanProgress.Backlog),
PlanRecordInProgressCount = planRecords.Count(x=>x.Progress == PlanProgress.InProgress),
PlanRecordTestingCount = planRecords.Count(x=>x.Progress == PlanProgress.Testing),
PlanRecordDoneCount = planRecords.Count(x=>x.Progress == PlanProgress.Done)
};
//set next reminder
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date))
{
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage))
{
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
apiResult.Add(resultToAdd);
}
return Json(apiResult);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/adjustedodometer")]
public IActionResult AdjustedOdometer(int vehicleId, int odometer)
{
var vehicle = _dataAccess.GetVehicleById(vehicleId);
if (vehicle == null || !vehicle.HasOdometerAdjustment)
{
return Json(odometer);
} else
{
var convertedOdometer = (odometer + int.Parse(vehicle.OdometerDifference)) * decimal.Parse(vehicle.OdometerMultiplier);
return Json(convertedOdometer);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/servicerecords")]
public IActionResult ServiceRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/servicerecords/add")]
public IActionResult AddServiceRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddServiceRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -128,7 +240,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -160,14 +273,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/repairrecords")]
public IActionResult RepairRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/repairrecords/add")]
public IActionResult AddRepairRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddRepairRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -196,7 +317,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -228,14 +350,22 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/upgraderecords")]
public IActionResult UpgradeRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
var result = vehicleRecords.Select(x => new GenericRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/upgraderecords/add")]
public IActionResult AddUpgradeRecord(int vehicleId, ServiceRecordExportModel input)
public IActionResult AddUpgradeRecord(int vehicleId, GenericRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
@@ -264,7 +394,8 @@ namespace CarCareTracker.Controllers
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -296,7 +427,15 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/taxrecords")]
public IActionResult TaxRecords(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -329,7 +468,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Tax Record via API - Description: {taxRecord.Description}");
@@ -350,7 +490,15 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/odometerrecords/latest")]
public IActionResult LastOdometer(int vehicleId)
{
var result = GetMaxMileage(vehicleId);
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var result = _vehicleLogic.GetMaxMileage(vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -358,13 +506,21 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/odometerrecords")]
public IActionResult OdometerRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
//determine if conversion is needed.
if (vehicleRecords.All(x => x.InitialMileage == default))
{
vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords);
}
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes });
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -396,7 +552,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()) : int.Parse(input.InitialOdometer),
Mileage = int.Parse(input.Odometer)
Mileage = int.Parse(input.Odometer),
ExtraFields = input.ExtraFields
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), vehicleId, User.Identity.Name, $"Added Odometer Record via API - Mileage: {odometerRecord.Mileage.ToString()}");
@@ -416,6 +573,14 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/gasrecords")]
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
{
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG)
.Select(x => new GasRecordExportModel {
@@ -426,7 +591,8 @@ namespace CarCareTracker.Controllers
FuelEconomy = x.MilesPerGallon.ToString(),
IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes
Notes = x.Notes,
ExtraFields = x.ExtraFields
});
return Json(result);
}
@@ -467,7 +633,8 @@ namespace CarCareTracker.Controllers
IsFillToFull = bool.Parse(input.IsFillToFull),
MissedFuelUp = bool.Parse(input.MissedFuelUp),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
Cost = decimal.Parse(input.Cost),
ExtraFields = input.ExtraFields
};
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
@@ -499,9 +666,17 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/reminders")]
public IActionResult Reminders(int vehicleId)
{
var currentMileage = GetMaxMileage(vehicleId);
if (vehicleId == default)
{
var response = new OperationResponse();
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes});
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString()});
return Json(results);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
@@ -511,11 +686,12 @@ namespace CarCareTracker.Controllers
{
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail;
foreach(Vehicle vehicle in vehicles)
{
var vehicleId = vehicle.Id;
//get reminders
var currentMileage = GetMaxMileage(vehicleId);
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).OrderByDescending(x => x.Urgency).ToList();
results.RemoveAll(x => !urgencies.Contains(x.Urgency));
@@ -526,6 +702,10 @@ namespace CarCareTracker.Controllers
//get list of recipients.
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
List<string> emailRecipients = new List<string>();
if (!string.IsNullOrWhiteSpace(defaultEmailAddress))
{
emailRecipients.Add(defaultEmailAddress);
}
foreach (int userId in userIds)
{
var userData = _userRecordDataAccess.GetUserRecordById(userId);
@@ -538,15 +718,19 @@ namespace CarCareTracker.Controllers
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
operationResponses.Add(result);
}
if (operationResponses.All(x => x.Success))
if (!operationResponses.Any())
{
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
return Json(new OperationResponse { Success = false, Message = "No Emails Sent, No Vehicles Available or No Recipients Configured" });
}
else if (operationResponses.All(x => x.Success))
{
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count()})" });
} else if (operationResponses.All(x => !x.Success))
{
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
return Json(new OperationResponse { Success = false, Message = $"All Emails Failed({operationResponses.Count()}), Check SMTP Settings" });
} else
{
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count(x => x.Success)}), Emails Failed({operationResponses.Count(x => !x.Success)}), Check Recipient Settings" });
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
@@ -559,41 +743,54 @@ namespace CarCareTracker.Controllers
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/cleanup")]
public IActionResult CleanUp(bool deepClean = false)
{
var jsonResponse = new Dictionary<string, string>();
//Clear out temp folder
var tempFilesDeleted = _fileHelper.ClearTempFolder();
jsonResponse.Add("temp_files_deleted", tempFilesDeleted.ToString());
if (deepClean)
{
//clear out unused vehicle thumbnails
var vehicles = _dataAccess.GetVehicles();
var vehicleImages = vehicles.Select(x => x.ImageLocation).Where(x => x.StartsWith("/images/")).Select(x=>Path.GetFileName(x)).ToList();
if (vehicleImages.Any())
{
var thumbnailsDeleted = _fileHelper.ClearUnlinkedThumbnails(vehicleImages);
jsonResponse.Add("unlinked_thumbnails_deleted", thumbnailsDeleted.ToString());
}
var vehicleDocuments = new List<string>();
foreach(Vehicle vehicle in vehicles)
{
vehicleDocuments.AddRange(_serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y=>Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_noteDataAccess.GetNotesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
vehicleDocuments.AddRange(_planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
}
//shop supplies
vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location)));
if (vehicleDocuments.Any())
{
var documentsDeleted = _fileHelper.ClearUnlinkedDocuments(vehicleDocuments);
jsonResponse.Add("unlinked_documents_deleted", documentsDeleted.ToString());
}
}
return Json(jsonResponse);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/demo/restore")]
public IActionResult RestoreDemo()
{
var result = _fileHelper.RestoreBackup("/defaults/demo_default.zip", true);
return Json(result);
}
private int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
}
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (repairRecords.Any())
{
numbersArray.Add(repairRecords.Max(x => x.Mileage));
}
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Max(x => x.Mileage));
}
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (upgradeRecords.Any())
{
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
}
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (odometerRecords.Any())
{
numbersArray.Add(odometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
}
}

View File

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

View File

@@ -92,5 +92,18 @@ namespace CarCareTracker.Controllers
}
return Path.Combine("/", uploadDirectory, fileName);
}
public IActionResult UploadCoordinates(List<string> coordinates)
{
string uploadDirectory = "temp/";
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
string fileName = Guid.NewGuid() + ".csv";
string filePath = Path.Combine(uploadPath, fileName);
string fileData = string.Join("\r\n", coordinates);
System.IO.File.WriteAllText(filePath, fileData);
var uploadedFile = new UploadedFiles { Name = "coordinates.csv", Location = Path.Combine("/", uploadDirectory, fileName) };
return Json(uploadedFile);
}
}
}

View File

@@ -16,20 +16,24 @@ namespace CarCareTracker.Controllers
private readonly IVehicleDataAccess _dataAccess;
private readonly IUserLogic _userLogic;
private readonly ILoginLogic _loginLogic;
private readonly IVehicleLogic _vehicleLogic;
private readonly IFileHelper _fileHelper;
private readonly IConfigHelper _config;
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly ITranslationHelper _translationHelper;
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
ILoginLogic loginLogic,
IVehicleLogic vehicleLogic,
IConfigHelper configuration,
IFileHelper fileHelper,
IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper)
IReminderHelper reminderHelper,
ITranslationHelper translationHelper)
{
_logger = logger;
_dataAccess = dataAccess;
@@ -40,6 +44,8 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
_loginLogic = loginLogic;
_vehicleLogic = vehicleLogic;
_translationHelper = translationHelper;
}
private int GetUserID()
{
@@ -56,7 +62,54 @@ namespace CarCareTracker.Controllers
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
return PartialView("_GarageDisplay", vehiclesStored);
var vehicleViewModels = vehiclesStored.Select(x =>
{
var vehicleVM = new VehicleViewModel
{
Id = x.Id,
ImageLocation = x.ImageLocation,
Year = x.Year,
Make = x.Make,
Model = x.Model,
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
OdometerOptional = x.OdometerOptional,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
DashboardMetrics = x.DashboardMetrics,
VehicleIdentifier = x.VehicleIdentifier
};
//dashboard metrics
if (x.DashboardMetrics.Any())
{
var vehicleRecords = _vehicleLogic.GetVehicleRecords(x.Id);
var userConfig = _config.GetUserConfig(User);
var distanceUnit = x.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km";
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.Default))
{
vehicleVM.LastReportedMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
vehicleVM.HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id, vehicleVM.LastReportedMileage);
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.CostPerMile))
{
var vehicleTotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var totalDistance = maxMileage - minMileage;
vehicleVM.CostPerMile = totalDistance != default ? vehicleTotalCost / totalDistance : 0.00M;
vehicleVM.DistanceUnit = distanceUnit;
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.TotalCost))
{
vehicleVM.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
}
}
return vehicleVM;
}).ToList();
return PartialView("_GarageDisplay", vehicleViewModels);
}
public IActionResult Calendar()
{
@@ -74,7 +127,7 @@ namespace CarCareTracker.Controllers
if (vehicleReminders.Any())
{
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, 0, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" }).ToList();
reminders.AddRange(reminderUrgency);
}
}
@@ -86,7 +139,7 @@ namespace CarCareTracker.Controllers
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, 0, DateTime.Now).FirstOrDefault();
return PartialView("_ReminderRecordCalendarModal", reminderUrgency);
}
public IActionResult Settings()
public async Task<IActionResult> Settings()
{
var userConfig = _config.GetUserConfig(User);
var languages = _fileHelper.GetLanguages();
@@ -97,6 +150,20 @@ namespace CarCareTracker.Controllers
};
return PartialView("_Settings", viewModel);
}
public async Task<IActionResult> Sponsors()
{
try
{
var httpClient = new HttpClient();
var sponsorsData = await httpClient.GetFromJsonAsync<Sponsors>(StaticHelper.SponsorsPath) ?? new Sponsors();
return PartialView("_Sponsors", sponsorsData);
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve sponsors: {ex.Message}");
return PartialView("_Sponsors", new Sponsors());
}
}
[HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig)
{
@@ -158,7 +225,8 @@ namespace CarCareTracker.Controllers
return Json(result);
}
return Json(false);
} catch (Exception ex)
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(false);
@@ -175,7 +243,7 @@ namespace CarCareTracker.Controllers
var result = _loginLogic.UpdateUserDetails(userId, userAccount);
return Json(result);
}
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage});
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
catch (Exception ex)
{
@@ -190,6 +258,216 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name;
return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName });
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetRootAccountInformationModal()
{
var userName = User.Identity.Name;
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetTranslatorEditor(string userLanguage)
{
var translationData = _translationHelper.GetTranslations(userLanguage);
return PartialView("_TranslationEditor", translationData);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult SaveTranslation(string userLanguage, Dictionary<string, string> translationData)
{
var result = _translationHelper.SaveTranslation(userLanguage, translationData);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult ExportTranslation(Dictionary<string, string> translationData)
{
var result = _translationHelper.ExportTranslation(translationData);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> GetAvailableTranslations()
{
try
{
var httpClient = new HttpClient();
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
return PartialView("_Translations", translations);
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
return PartialView("_Translations", new Translations());
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> DownloadTranslation(string continent, string name)
{
try
{
var httpClient = new HttpClient();
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath(continent, name)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(name, translationData);
if (!result.Success)
{
return Json(false);
}
}
else
{
_logger.LogError($"Unable to download translation: {name}");
return Json(false);
}
return Json(true);
}
catch (Exception ex)
{
_logger.LogError($"Unable to download translation: {ex.Message}");
return Json(false);
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public async Task<IActionResult> DownloadAllTranslations()
{
try
{
var httpClient = new HttpClient();
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
int translationsDownloaded = 0;
foreach (string translation in translations.Asia)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Asia", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Africa)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Africa", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Europe)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Europe", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.NorthAmerica)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("NorthAmerica", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.SouthAmerica)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("SouthAmerica", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
foreach (string translation in translations.Oceania)
{
try
{
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Oceania", translation)) ?? new Dictionary<string, string>();
if (translationData.Any())
{
var result = _translationHelper.SaveTranslation(translation, translationData);
if (result.Success)
{
translationsDownloaded++;
};
}
}
catch (Exception ex)
{
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
}
}
if (translationsDownloaded > 0)
{
return Json(new OperationResponse() { Success = true, Message = $"{translationsDownloaded} Translations Downloaded" });
} else
{
return Json(new OperationResponse() { Success = false, Message = "No Translations Downloaded" });
}
}
catch (Exception ex)
{
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
return Json(new OperationResponse() { Success = false, Message = StaticHelper.GenericErrorMessage });
}
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{

View File

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

View File

@@ -34,6 +34,7 @@ namespace CarCareTracker.Controllers
{
var cmds = new List<string>
{
"CREATE SCHEMA IF NOT EXISTS app",
"CREATE TABLE IF NOT EXISTS app.vehicles (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.collisionrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.upgraderecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.OrderByDescending(x => x.Pinned).ToList();
return PartialView("_Notes", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.Where(x => x.Pinned).ToList();
return Json(result);
}
[HttpPost]
public IActionResult SaveNoteToVehicleId(Note note)
{
note.Files = note.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _noteDataAccess.SaveNoteToVehicle(note);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), note.VehicleId, User.Identity.Name, $"{(note.Id == default ? "Created" : "Edited")} Note - Description: {note.Description}");
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddNotePartialView()
{
return PartialView("_NoteModal", new Note());
}
[HttpGet]
public IActionResult GetNoteForEditById(int noteId)
{
var result = _noteDataAccess.GetNoteById(noteId);
return PartialView("_NoteModal", result);
}
[HttpPost]
public IActionResult DeleteNoteById(int noteId)
{
var result = _noteDataAccess.DeleteNoteById(noteId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Note - Id: {noteId}");
}
return Json(result);
}
[HttpPost]
public IActionResult PinNotes(List<int> noteIds, bool isToggle = false, bool pinStatus = false)
{
var result = false;
foreach (int noteId in noteIds)
{
var existingNote = _noteDataAccess.GetNoteById(noteId);
if (isToggle)
{
existingNote.Pinned = !existingNote.Pinned;
}
else
{
existingNote.Pinned = pinStatus;
}
result = _noteDataAccess.SaveNoteToVehicle(existingNote);
}
return Json(result);
}
}
}

View File

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

View File

@@ -0,0 +1,243 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPlanRecordsByVehicleId(int vehicleId)
{
var result = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
return PartialView("_PlanRecords", result);
}
[HttpPost]
public IActionResult SavePlanRecordToVehicleId(PlanRecordInput planRecord)
{
//populate createdDate
if (planRecord.Id == default)
{
planRecord.DateCreated = DateTime.Now.ToString("G");
}
planRecord.DateModified = DateTime.Now.ToString("G");
//move files from temp.
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
if (planRecord.Supplies.Any())
{
planRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description);
if (planRecord.CopySuppliesAttachment)
{
planRecord.Files.AddRange(GetSuppliesAttachments(planRecord.Supplies));
}
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), planRecord.VehicleId, User.Identity.Name, $"{(planRecord.Id == default ? "Created" : "Edited")} Plan Record - Description: {planRecord.Description}");
}
return Json(result);
}
[HttpPost]
public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord)
{
//check if template name already taken.
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
if (planRecord.Id == default && existingRecord)
{
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle" });
}
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
return Json(new OperationResponse { Success = result, Message = result ? "Template Added" : StaticHelper.GenericErrorMessage });
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPlanRecordTemplatesForVehicleId(int vehicleId)
{
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicleId);
return PartialView("_PlanRecordTemplateModal", result);
}
[HttpPost]
public IActionResult DeletePlanRecordTemplateById(int planRecordTemplateId)
{
var result = _planRecordTemplateDataAccess.DeletePlanRecordTemplateById(planRecordTemplateId);
return Json(result);
}
[HttpPost]
public IActionResult ConvertPlanRecordTemplateToPlanRecord(int planRecordTemplateId)
{
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (existingRecord.Id == default)
{
return Json(new OperationResponse { Success = false, Message = "Unable to find template" });
}
if (existingRecord.Supplies.Any())
{
//check if all supplies are available
var supplyAvailability = CheckSupplyRecordsAvailability(existingRecord.Supplies);
if (supplyAvailability.Any())
{
return Json(new OperationResponse { Success = false, Message = string.Join("<br>", supplyAvailability) });
}
}
if (existingRecord.ReminderRecordId != default)
{
//check if reminder still exists and is still recurring.
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(existingRecord.ReminderRecordId);
if (existingReminder is null || existingReminder.Id == default || !existingReminder.IsRecurring)
{
return Json(new OperationResponse { Success = false, Message = "Missing or Non-recurring Reminder, Please Delete This Template and Recreate It." });
}
}
//populate createdDate
existingRecord.DateCreated = DateTime.Now.ToString("G");
existingRecord.DateModified = DateTime.Now.ToString("G");
existingRecord.Id = default;
if (existingRecord.Supplies.Any())
{
existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description);
if (existingRecord.CopySuppliesAttachment)
{
existingRecord.Files.AddRange(GetSuppliesAttachments(existingRecord.Supplies));
}
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
}
[HttpGet]
public IActionResult GetAddPlanRecordPartialView()
{
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
}
[HttpPost]
public IActionResult GetAddPlanRecordPartialView(PlanRecordInput? planModel)
{
if (planModel is not null)
{
planModel.ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields;
return PartialView("_PlanRecordModal", planModel);
}
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
}
[HttpPost]
public IActionResult UpdatePlanRecordProgress(int planRecordId, PlanProgress planProgress, int odometer = 0)
{
var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId);
existingRecord.Progress = planProgress;
existingRecord.DateModified = DateTime.Now;
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
if (planProgress == PlanProgress.Done)
{
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
{
Date = DateTime.Now.Date,
VehicleId = existingRecord.VehicleId,
Mileage = odometer,
Notes = $"Auto Insert From Plan Record: {existingRecord.Description}",
ExtraFields = existingRecord.ExtraFields
});
}
//convert plan record to service/upgrade/repair record.
if (existingRecord.ImportMode == ImportMode.ServiceRecord)
{
var newRecord = new ServiceRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
}
else if (existingRecord.ImportMode == ImportMode.RepairRecord)
{
var newRecord = new CollisionRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
}
else if (existingRecord.ImportMode == ImportMode.UpgradeRecord)
{
var newRecord = new UpgradeRecord()
{
VehicleId = existingRecord.VehicleId,
Date = DateTime.Now.Date,
Mileage = odometer,
Description = existingRecord.Description,
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(newRecord);
}
//push back any reminders
if (existingRecord.ReminderRecordId != default)
{
PushbackRecurringReminderRecordWithChecks(existingRecord.ReminderRecordId, DateTime.Now, odometer);
}
}
return Json(result);
}
[HttpGet]
public IActionResult GetPlanRecordTemplateForEditById(int planRecordTemplateId)
{
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
return PartialView("_PlanRecordTemplateEditModal", result);
}
[HttpGet]
public IActionResult GetPlanRecordForEditById(int planRecordId)
{
var result = _planRecordDataAccess.GetPlanRecordById(planRecordId);
//convert to Input object.
var convertedResult = new PlanRecordInput
{
Id = result.Id,
Description = result.Description,
DateCreated = result.DateCreated.ToString("G"),
DateModified = result.DateModified.ToString("G"),
ImportMode = result.ImportMode,
Priority = result.Priority,
Progress = result.Progress,
Cost = result.Cost,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
RequisitionHistory = result.RequisitionHistory,
ReminderRecordId = result.ReminderRecordId,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields)
};
return PartialView("_PlanRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeletePlanRecordById(int planRecordId)
{
var result = _planRecordDataAccess.DeletePlanRecordById(planRecordId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Plan Record - Id: {planRecordId}");
}
return Json(result);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,194 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
private List<string> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
{
//returns empty string if all supplies are available
var result = new List<string>();
foreach (SupplyUsage supply in supplyUsage)
{
//get supply record.
var supplyData = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
if (supplyData == null)
{
result.Add("Missing Supplies, Please Delete This Template and Recreate It.");
}
else if (supply.Quantity > supplyData.Quantity)
{
result.Add($"Insufficient Quantity for {supplyData.Description}, need: {supply.Quantity}, available: {supplyData.Quantity}");
}
}
return result;
}
private List<UploadedFiles> GetSuppliesAttachments(List<SupplyUsage> supplyUsage)
{
List<UploadedFiles> results = new List<UploadedFiles>();
foreach (SupplyUsage supply in supplyUsage)
{
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
results.AddRange(result.Files);
}
return results;
}
private List<SupplyUsageHistory> RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage, DateTime dateRequisitioned, string usageDescription)
{
List<SupplyUsageHistory> results = new List<SupplyUsageHistory>();
foreach (SupplyUsage supply in supplyUsage)
{
//get supply record.
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
var unitCost = (result.Quantity != 0) ? result.Cost / result.Quantity : 0;
//deduct quantity used.
result.Quantity -= supply.Quantity;
//deduct cost.
result.Cost -= (supply.Quantity * unitCost);
//check decimal places to ensure that it always has a max of 3 decimal places.
var roundedDecimal = decimal.Round(result.Cost, 3);
if (roundedDecimal != result.Cost)
{
//Too many decimals
result.Cost = roundedDecimal;
}
//create new requisitionrrecord
var requisitionRecord = new SupplyUsageHistory
{
Date = dateRequisitioned,
Description = usageDescription,
Quantity = supply.Quantity,
Cost = (supply.Quantity * unitCost)
};
result.RequisitionHistory.Add(requisitionRecord);
//save
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(result);
requisitionRecord.Description = result.Description; //change the name of the description for plan/service/repair/upgrade records
requisitionRecord.PartNumber = result.PartNumber; //populate part number if not displayed in supplies modal.
results.Add(requisitionRecord);
}
return results;
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetSupplyRecordsByVehicleId(int vehicleId)
{
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
return PartialView("_SupplyRecords", result);
}
[HttpGet]
public IActionResult GetSupplyRecordsForPlanRecordTemplate(int planRecordTemplateId)
{
var viewModel = new SupplyUsageViewModel();
var planRecordTemplate = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (planRecordTemplate != default && planRecordTemplate.VehicleId != default)
{
var supplies = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(planRecordTemplate.VehicleId);
if (_config.GetServerEnableShopSupplies())
{
supplies.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
}
supplies.RemoveAll(x => x.Quantity <= 0);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
supplies = supplies.OrderByDescending(x => x.Date).ToList();
}
else
{
supplies = supplies.OrderBy(x => x.Date).ToList();
}
viewModel.Supplies = supplies;
viewModel.Usage = planRecordTemplate.Supplies;
}
return PartialView("_SupplyUsage", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetSupplyRecordsForRecordsByVehicleId(int vehicleId)
{
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
if (_config.GetServerEnableShopSupplies())
{
result.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
}
result.RemoveAll(x => x.Quantity <= 0);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
var viewModel = new SupplyUsageViewModel
{
Supplies = result
};
return PartialView("_SupplyUsage", viewModel);
}
[HttpPost]
public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord)
{
//move files from temp.
supplyRecord.Files = supplyRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(supplyRecord.ToSupplyRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), supplyRecord.VehicleId, User.Identity.Name, $"{(supplyRecord.Id == default ? "Created" : "Edited")} Supply Record - Description: {supplyRecord.Description}");
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddSupplyRecordPartialView()
{
return PartialView("_SupplyRecordModal", new SupplyRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetSupplyRecordForEditById(int supplyRecordId)
{
var result = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId);
//convert to Input object.
var convertedResult = new SupplyRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
PartNumber = result.PartNumber,
Quantity = result.Quantity,
PartSupplier = result.PartSupplier,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files,
Tags = result.Tags,
RequisitionHistory = result.RequisitionHistory,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields)
};
return PartialView("_SupplyRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeleteSupplyRecordById(int supplyRecordId)
{
var result = _supplyRecordDataAccess.DeleteSupplyRecordById(supplyRecordId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Supply Record - Id: {supplyRecordId}");
}
return Json(result);
}
}
}

View File

@@ -0,0 +1,124 @@
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public partial class VehicleController
{
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetTaxRecordsByVehicleId(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
bool _useDescending = _config.GetUserConfig(User).UseDescending;
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ToList();
}
return PartialView("_TaxRecords", result);
}
private void UpdateRecurringTaxes(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var recurringFees = result.Where(x => x.IsRecurring);
if (recurringFees.Any())
{
foreach (TaxRecord recurringFee in recurringFees)
{
var newDate = new DateTime();
if (recurringFee.RecurringInterval != ReminderMonthInterval.Other)
{
newDate = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
}
else
{
newDate = recurringFee.Date.AddMonths(recurringFee.CustomMonthInterval);
}
if (DateTime.Now > newDate)
{
recurringFee.IsRecurring = false;
var newRecurringFee = new TaxRecord()
{
VehicleId = recurringFee.VehicleId,
Date = newDate,
Description = recurringFee.Description,
Cost = recurringFee.Cost,
IsRecurring = true,
Notes = recurringFee.Notes,
RecurringInterval = recurringFee.RecurringInterval,
CustomMonthInterval = recurringFee.CustomMonthInterval,
Files = recurringFee.Files,
Tags = recurringFee.Tags,
ExtraFields = recurringFee.ExtraFields
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
}
}
}
}
[HttpPost]
public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord)
{
//move files from temp.
taxRecord.Files = taxRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
//push back any reminders
if (taxRecord.ReminderRecordId.Any())
{
foreach (int reminderRecordId in taxRecord.ReminderRecordId)
{
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(taxRecord.Date), null);
}
}
var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord());
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), taxRecord.VehicleId, User.Identity.Name, $"{(taxRecord.Id == default ? "Created" : "Edited")} Tax Record - Description: {taxRecord.Description}");
}
return Json(result);
}
[HttpGet]
public IActionResult GetAddTaxRecordPartialView()
{
return PartialView("_TaxRecordModal", new TaxRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields });
}
[HttpGet]
public IActionResult GetTaxRecordForEditById(int taxRecordId)
{
var result = _taxRecordDataAccess.GetTaxRecordById(taxRecordId);
//convert to Input object.
var convertedResult = new TaxRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Notes = result.Notes,
VehicleId = result.VehicleId,
IsRecurring = result.IsRecurring,
RecurringInterval = result.RecurringInterval,
CustomMonthInterval = result.CustomMonthInterval,
Files = result.Files,
Tags = result.Tags,
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields)
};
return PartialView("_TaxRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeleteTaxRecordById(int taxRecordId)
{
var result = _taxRecordDataAccess.DeleteTaxRecordById(taxRecordId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Tax Record - Id: {taxRecordId}");
}
return Json(result);
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

9
Enum/DashboardMetric.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();
@@ -176,7 +176,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("vehicleId", vehicleId);
return ctext.ExecuteNonQuery() > 0;
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)
@@ -198,7 +199,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("userId", userId);
return ctext.ExecuteNonQuery() > 0;
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)

View File

@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();
@@ -94,7 +94,8 @@ namespace CarCareTracker.External.Implementations
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", userId);
return ctext.ExecuteNonQuery() > 0;
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)

View File

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

View File

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

View File

@@ -12,10 +12,12 @@ namespace CarCareTracker.Helper
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
bool AuthenticateRootUser(string username, string password);
bool AuthenticateRootUserOIDC(string email);
string GetWebHookUrl();
string GetMOTD();
string GetLogoUrl();
string GetServerLanguage();
bool GetServerDisabledRegistration();
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
@@ -89,11 +91,26 @@ namespace CarCareTracker.Helper
}
return username == rootUsername && password == rootPassword;
}
public bool AuthenticateRootUserOIDC(string email)
{
var rootEmail = _config[nameof(UserConfig.DefaultReminderEmail)] ?? string.Empty;
var rootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]);
if (!rootUserOIDC || string.IsNullOrWhiteSpace(rootEmail))
{
return false;
}
return email == rootEmail;
}
public string GetServerLanguage()
{
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
return serverLanguage;
}
public bool GetServerDisabledRegistration()
{
var registrationDisabled = bool.Parse(_config[nameof(UserConfig.DisableRegistration)]);
return registrationDisabled;
}
public string GetServerPostgresConnection()
{
if (!string.IsNullOrWhiteSpace(_config["POSTGRES_CONNECTION"]))
@@ -162,9 +179,11 @@ namespace CarCareTracker.Helper
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseSystemColorMode = bool.Parse(_config[nameof(UserConfig.UseSystemColorMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
EnableRootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
@@ -178,9 +197,12 @@ namespace CarCareTracker.Helper
EnableShopSupplies = bool.Parse(_config[nameof(UserConfig.EnableShopSupplies)]),
EnableExtraFieldColumns = bool.Parse(_config[nameof(UserConfig.EnableExtraFieldColumns)]),
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>(),
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>(),
UserColumnPreferences = _config.GetSection(nameof(UserConfig.UserColumnPreferences)).Get<List<UserColumnPreference>>() ?? new List<UserColumnPreference>(),
ReminderUrgencyConfig = _config.GetSection(nameof(UserConfig.ReminderUrgencyConfig)).Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig(),
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)]),
DefaultReminderEmail = _config[nameof(UserConfig.DefaultReminderEmail)],
DisableRegistration = bool.Parse(_config[nameof(UserConfig.DisableRegistration)])
};
int userId = 0;
if (user != null)

View File

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

View File

@@ -57,6 +57,10 @@ namespace CarCareTracker.Helper
if (i > 0)
{
var deltaMileage = currentObject.Mileage - previousMileage;
if (deltaMileage < 0)
{
deltaMileage = 0;
}
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,

View File

@@ -1,6 +1,6 @@
using CarCareTracker.Models;
using System.Net.Mail;
using System.Net;
using MimeKit;
using MailKit.Net.Smtp;
namespace CarCareTracker.Helper
{
@@ -15,13 +15,16 @@ namespace CarCareTracker.Helper
{
private readonly MailConfig mailConfig;
private readonly IFileHelper _fileHelper;
private readonly ILogger<MailHelper> _logger;
public MailHelper(
IConfiguration config,
IFileHelper fileHelper
IFileHelper fileHelper,
ILogger<MailHelper> logger
) {
//load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
mailConfig = config.GetSection("MailConfig").Get<MailConfig>() ?? new MailConfig();
_fileHelper = fileHelper;
_logger = logger;
}
public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{
@@ -34,7 +37,7 @@ namespace CarCareTracker.Helper
}
string emailSubject = "Your Registration Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -55,7 +58,7 @@ namespace CarCareTracker.Helper
}
string emailSubject = "Your Password Reset Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -77,7 +80,7 @@ namespace CarCareTracker.Helper
}
string emailSubject = "Your User Account Update Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please update your account for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
var result = SendEmail(new List<string> { emailAddress}, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
@@ -106,53 +109,64 @@ namespace CarCareTracker.Helper
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
//construct html table.
string emailBody = File.ReadAllText(emailTemplatePath);
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}");
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}");
string tableBody = "";
foreach(ReminderRecordViewModel reminder in reminders)
{
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
tableBody += $"<tr class='{reminder.Urgency}'><td>{StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency)}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
}
emailBody = emailBody.Replace("{TableBody}", tableBody);
try
{
foreach (string emailAddress in emailAddresses)
var result = SendEmail(emailAddresses, emailSubject, emailBody);
if (result)
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
return new OperationResponse { Success = true, Message = "Email Sent!" };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) {
string to = emailTo;
private bool SendEmail(List<string> emailTo, string emailSubject, string emailBody) {
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
message.IsBodyHtml = isBodyHtml;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
var message = new MimeMessage();
message.From.Add(new MailboxAddress(from, from));
foreach(string emailRecipient in emailTo)
{
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
message.To.Add(new MailboxAddress(emailRecipient, emailRecipient));
}
message.Subject = emailSubject;
var builder = new BodyBuilder();
builder.HtmlBody = emailBody;
message.Body = builder.ToMessageBody();
using (var client = new SmtpClient())
{
client.Connect(server, mailConfig.Port, MailKit.Security.SecureSocketOptions.Auto);
//perform authentication if either username or password is provided.
//do not perform authentication if neither are provided.
if (!string.IsNullOrWhiteSpace(mailConfig.Username) || !string.IsNullOrWhiteSpace(mailConfig.Password)) {
client.Authenticate(mailConfig.Username, mailConfig.Password);
}
else
try
{
client.Send(message);
client.Disconnect(true);
return true;
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
return true;
}
catch (Exception ex)
{
return false;
}
}
}

View File

@@ -4,7 +4,7 @@ namespace CarCareTracker.Helper
{
public interface IReminderHelper
{
ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder);
ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder, DateTime? currentDate, int? currentMileage);
List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare);
}
public class ReminderHelper: IReminderHelper
@@ -14,46 +14,48 @@ namespace CarCareTracker.Helper
{
_config = config;
}
public ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder)
public ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder, DateTime? currentDate, int? currentMileage)
{
var newDate = currentDate ?? existingReminder.Date;
var newMileage = currentMileage ?? existingReminder.Mileage;
if (existingReminder.Metric == ReminderMetric.Both)
{
if (existingReminder.ReminderMonthInterval != ReminderMonthInterval.Other)
{
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval);
} else
{
existingReminder.Date = existingReminder.Date.AddMonths(existingReminder.CustomMonthInterval);
existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval);
}
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
{
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
existingReminder.Mileage = newMileage + (int)existingReminder.ReminderMileageInterval;
}
else
{
existingReminder.Mileage += existingReminder.CustomMileageInterval;
existingReminder.Mileage = newMileage + existingReminder.CustomMileageInterval;
}
}
else if (existingReminder.Metric == ReminderMetric.Odometer)
{
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
{
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
existingReminder.Mileage = newMileage + (int)existingReminder.ReminderMileageInterval;
} else
{
existingReminder.Mileage += existingReminder.CustomMileageInterval;
existingReminder.Mileage = newMileage + existingReminder.CustomMileageInterval;
}
}
else if (existingReminder.Metric == ReminderMetric.Date)
{
if (existingReminder.ReminderMonthInterval != ReminderMonthInterval.Other)
{
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval);
}
else
{
existingReminder.Date = existingReminder.Date.AddMonths(existingReminder.CustomMonthInterval);
existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval);
}
}
return existingReminder;
@@ -64,6 +66,10 @@ namespace CarCareTracker.Helper
var reminderUrgencyConfig = _config.GetReminderUrgencyConfig();
foreach (var reminder in reminders)
{
if (reminder.UseCustomThresholds)
{
reminderUrgencyConfig = reminder.CustomThresholds;
}
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,
@@ -73,7 +79,8 @@ namespace CarCareTracker.Helper
Description = reminder.Description,
Notes = reminder.Notes,
Metric = reminder.Metric,
IsRecurring = reminder.IsRecurring
IsRecurring = reminder.IsRecurring,
Tags = reminder.Tags
};
if (reminder.Metric == ReminderMetric.Both)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models;
using CsvHelper;
using System.Globalization;
namespace CarCareTracker.Helper
@@ -8,13 +9,15 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.2.8";
public static string VersionNumber = "1.3.9";
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public static string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public static string TranslationPath = "https://hargata.github.io/lubelog_translations";
public static string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
{
switch (input)
@@ -118,6 +121,10 @@ namespace CarCareTracker.Helper
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
};
}
public static List<string> GetBarChartColors()
{
return new List<string> { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
}
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
{
@@ -198,6 +205,8 @@ namespace CarCareTracker.Helper
recordExtraFields.Add(extraField);
}
}
//re-order extra fields
recordExtraFields = recordExtraFields.OrderBy(x => templateExtraFields.FindIndex(y => y.Name == x.Name)).ToList();
return recordExtraFields;
}
@@ -243,6 +252,10 @@ namespace CarCareTracker.Helper
}
var motd = config["LUBELOGGER_MOTD"] ?? "Not Configured";
Console.WriteLine($"Message Of The Day: {motd}");
if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name))
{
Console.WriteLine("No Locale or Culture Configured for LubeLogger, Check Environment Variables");
}
}
public static async void NotifyAsync(string webhookURL, int vehicleId, string username, string action)
{
@@ -259,5 +272,309 @@ namespace CarCareTracker.Helper
};
httpClient.PostAsJsonAsync(webhookURL, httpParams);
}
public static string GetImportModeIcon(ImportMode importMode)
{
switch (importMode)
{
case ImportMode.ServiceRecord:
return "bi-card-checklist";
case ImportMode.RepairRecord:
return "bi-exclamation-octagon";
case ImportMode.UpgradeRecord:
return "bi-wrench-adjustable";
case ImportMode.TaxRecord:
return "bi-currency-dollar";
case ImportMode.SupplyRecord:
return "bi-shop";
case ImportMode.PlanRecord:
return "bi-bar-chart-steps";
case ImportMode.OdometerRecord:
return "bi-speedometer";
case ImportMode.GasRecord:
return "bi-fuel-pump";
case ImportMode.NoteRecord:
return "bi-journal-bookmark";
case ImportMode.ReminderRecord:
return "bi-bell";
default:
return "bi-file-bar-graph";
}
}
public static string GetVehicleIdentifier(Vehicle vehicle)
{
if (vehicle.VehicleIdentifier == "LicensePlate")
{
return vehicle.LicensePlate;
} else
{
if (vehicle.ExtraFields.Any(x=>x.Name == vehicle.VehicleIdentifier))
{
return vehicle.ExtraFields?.FirstOrDefault(x=>x.Name == vehicle.VehicleIdentifier)?.Value;
} else
{
return "N/A";
}
}
}
public static string GetVehicleIdentifier(VehicleViewModel vehicle)
{
if (vehicle.VehicleIdentifier == "LicensePlate")
{
return vehicle.LicensePlate;
}
else
{
if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier))
{
return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value;
}
else
{
return "N/A";
}
}
}
//Translations
public static string GetTranslationDownloadPath(string continent, string name)
{
if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name)){
return string.Empty;
}
else
{
switch (continent)
{
case "NorthAmerica":
continent = "North America";
break;
case "SouthAmerica":
continent = "South America";
break;
}
return $"{TranslationPath}/{continent}/{name}.json";
}
}
public static string GetTranslationName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return string.Empty;
} else
{
try
{
string cleanedName = name.Contains("_") ? name.Replace("_", "-") : name;
string displayName = CultureInfo.GetCultureInfo(cleanedName).DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
return name;
}
else
{
return displayName;
}
} catch (Exception ex)
{
return name;
}
}
}
//CSV Write Methods
public static void WriteGenericRecordExportModel(CsvWriter _csv, IEnumerable<GenericRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(GenericRecordExportModel.Date));
_csv.WriteField(nameof(GenericRecordExportModel.Description));
_csv.WriteField(nameof(GenericRecordExportModel.Cost));
_csv.WriteField(nameof(GenericRecordExportModel.Notes));
_csv.WriteField(nameof(GenericRecordExportModel.Odometer));
_csv.WriteField(nameof(GenericRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (GenericRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteOdometerRecordExportModel(CsvWriter _csv, IEnumerable<OdometerRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(OdometerRecordExportModel.Date));
_csv.WriteField(nameof(OdometerRecordExportModel.InitialOdometer));
_csv.WriteField(nameof(OdometerRecordExportModel.Odometer));
_csv.WriteField(nameof(OdometerRecordExportModel.Notes));
_csv.WriteField(nameof(OdometerRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (OdometerRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.InitialOdometer);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteTaxRecordExportModel(CsvWriter _csv, IEnumerable<TaxRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(TaxRecordExportModel.Date));
_csv.WriteField(nameof(TaxRecordExportModel.Description));
_csv.WriteField(nameof(TaxRecordExportModel.Cost));
_csv.WriteField(nameof(TaxRecordExportModel.Notes));
_csv.WriteField(nameof(TaxRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (TaxRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteSupplyRecordExportModel(CsvWriter _csv, IEnumerable<SupplyRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(SupplyRecordExportModel.Date));
_csv.WriteField(nameof(SupplyRecordExportModel.PartNumber));
_csv.WriteField(nameof(SupplyRecordExportModel.PartSupplier));
_csv.WriteField(nameof(SupplyRecordExportModel.PartQuantity));
_csv.WriteField(nameof(SupplyRecordExportModel.Description));
_csv.WriteField(nameof(SupplyRecordExportModel.Notes));
_csv.WriteField(nameof(SupplyRecordExportModel.Cost));
_csv.WriteField(nameof(SupplyRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (SupplyRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.PartNumber);
_csv.WriteField(genericRecord.PartSupplier);
_csv.WriteField(genericRecord.PartQuantity);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WritePlanRecordExportModel(CsvWriter _csv, IEnumerable<PlanRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(PlanRecordExportModel.DateCreated));
_csv.WriteField(nameof(PlanRecordExportModel.DateModified));
_csv.WriteField(nameof(PlanRecordExportModel.Description));
_csv.WriteField(nameof(PlanRecordExportModel.Notes));
_csv.WriteField(nameof(PlanRecordExportModel.Type));
_csv.WriteField(nameof(PlanRecordExportModel.Priority));
_csv.WriteField(nameof(PlanRecordExportModel.Progress));
_csv.WriteField(nameof(PlanRecordExportModel.Cost));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (PlanRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.DateCreated);
_csv.WriteField(genericRecord.DateModified);
_csv.WriteField(genericRecord.Description);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Type);
_csv.WriteField(genericRecord.Priority);
_csv.WriteField(genericRecord.Progress);
_csv.WriteField(genericRecord.Cost);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
public static void WriteGasRecordExportModel(CsvWriter _csv, IEnumerable<GasRecordExportModel> genericRecords)
{
var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct();
//write headers
_csv.WriteField(nameof(GasRecordExportModel.Date));
_csv.WriteField(nameof(GasRecordExportModel.Odometer));
_csv.WriteField(nameof(GasRecordExportModel.FuelConsumed));
_csv.WriteField(nameof(GasRecordExportModel.Cost));
_csv.WriteField(nameof(GasRecordExportModel.FuelEconomy));
_csv.WriteField(nameof(GasRecordExportModel.IsFillToFull));
_csv.WriteField(nameof(GasRecordExportModel.MissedFuelUp));
_csv.WriteField(nameof(GasRecordExportModel.Notes));
_csv.WriteField(nameof(GasRecordExportModel.Tags));
foreach (string extraHeader in extraHeaders)
{
_csv.WriteField($"extrafield_{extraHeader}");
}
_csv.NextRecord();
foreach (GasRecordExportModel genericRecord in genericRecords)
{
_csv.WriteField(genericRecord.Date);
_csv.WriteField(genericRecord.Odometer);
_csv.WriteField(genericRecord.FuelConsumed);
_csv.WriteField(genericRecord.Cost);
_csv.WriteField(genericRecord.FuelEconomy);
_csv.WriteField(genericRecord.IsFillToFull);
_csv.WriteField(genericRecord.MissedFuelUp);
_csv.WriteField(genericRecord.Notes);
_csv.WriteField(genericRecord.Tags);
foreach (string extraHeader in extraHeaders)
{
var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault();
_csv.WriteField(extraField != null ? extraField.Value : string.Empty);
}
_csv.NextRecord();
}
}
}
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -28,7 +29,7 @@ namespace CarCareTracker.Logic
bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset);
List<UserData> GetAllUsers();
List<Token> GetAllTokens();
KeyValuePair<string, string> GetPKCEChallengeCode();
}
public class LoginLogic : ILoginLogic
{
@@ -244,14 +245,7 @@ namespace CarCareTracker.Logic
{
if (UserIsRoot(credentials))
{
return new UserData()
{
Id = -1,
UserName = credentials.UserName,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
return GetRootUserData(credentials.UserName);
}
else
{
@@ -270,6 +264,13 @@ namespace CarCareTracker.Logic
}
public UserData ValidateOpenIDUser(LoginModel credentials)
{
//validate for root user
var isRootUser = _configHelper.AuthenticateRootUserOIDC(credentials.EmailAddress);
if (isRootUser)
{
return GetRootUserData(credentials.EmailAddress);
}
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (result.Id != default)
{
@@ -419,6 +420,17 @@ namespace CarCareTracker.Logic
var hashedPassword = GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
}
private UserData GetRootUserData(string username)
{
return new UserData()
{
Id = -1,
UserName = username,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
}
#endregion
private static string GetHash(string value)
{
@@ -439,6 +451,14 @@ namespace CarCareTracker.Logic
{
return Guid.NewGuid().ToString().Substring(0, 8);
}
public KeyValuePair<string, string> GetPKCEChallengeCode()
{
var verifierCode = Base64UrlEncoder.Encode(Guid.NewGuid().ToString().Replace("-", ""));
var verifierBytes = Encoding.UTF8.GetBytes(verifierCode);
var hashedCode = SHA256.Create().ComputeHash(verifierBytes);
var encodedChallengeCode = Base64UrlEncoder.Encode(hashedCode);
return new KeyValuePair<string, string>(verifierCode, encodedChallengeCode);
}
public bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset)
{
bool result = false;

View File

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

222
Logic/VehicleLogic.cs Normal file
View File

@@ -0,0 +1,222 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
namespace CarCareTracker.Logic
{
public interface IVehicleLogic
{
VehicleRecords GetVehicleRecords(int vehicleId);
decimal GetVehicleTotalCost(VehicleRecords vehicleRecords);
int GetMaxMileage(int vehicleId);
int GetMaxMileage(VehicleRecords vehicleRecords);
int GetMinMileage(int vehicleId);
int GetMinMileage(VehicleRecords vehicleRecords);
int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage);
}
public class VehicleLogic: IVehicleLogic
{
private readonly IServiceRecordDataAccess _serviceRecordDataAccess;
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
public VehicleLogic(
IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper
) {
_serviceRecordDataAccess = serviceRecordDataAccess;
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
}
public VehicleRecords GetVehicleRecords(int vehicleId)
{
return new VehicleRecords
{
ServiceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId),
GasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId),
CollisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId),
TaxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId),
UpgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId),
OdometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId),
};
}
public decimal GetVehicleTotalCost(VehicleRecords vehicleRecords)
{
var serviceRecordSum = vehicleRecords.ServiceRecords.Sum(x => x.Cost);
var repairRecordSum = vehicleRecords.CollisionRecords.Sum(x => x.Cost);
var upgradeRecordSum = vehicleRecords.UpgradeRecords.Sum(x => x.Cost);
var taxRecordSum = vehicleRecords.TaxRecords.Sum(x => x.Cost);
var gasRecordSum = vehicleRecords.GasRecords.Sum(x => x.Cost);
return serviceRecordSum + repairRecordSum + upgradeRecordSum + taxRecordSum + gasRecordSum;
}
public int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
}
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (repairRecords.Any())
{
numbersArray.Add(repairRecords.Max(x => x.Mileage));
}
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Max(x => x.Mileage));
}
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (upgradeRecords.Any())
{
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
}
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (odometerRecords.Any())
{
numbersArray.Add(odometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMaxMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
if (vehicleRecords.ServiceRecords.Any())
{
numbersArray.Add(vehicleRecords.ServiceRecords.Max(x => x.Mileage));
}
if (vehicleRecords.CollisionRecords.Any())
{
numbersArray.Add(vehicleRecords.CollisionRecords.Max(x => x.Mileage));
}
if (vehicleRecords.GasRecords.Any())
{
numbersArray.Add(vehicleRecords.GasRecords.Max(x => x.Mileage));
}
if (vehicleRecords.UpgradeRecords.Any())
{
numbersArray.Add(vehicleRecords.UpgradeRecords.Max(x => x.Mileage));
}
if (vehicleRecords.OdometerRecords.Any())
{
numbersArray.Add(vehicleRecords.OdometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMinMileage(int vehicleId)
{
var numbersArray = new List<int>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default);
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Min(x => x.Mileage));
}
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default);
if (repairRecords.Any())
{
numbersArray.Add(repairRecords.Min(x => x.Mileage));
}
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default);
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Min(x => x.Mileage));
}
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default);
if (upgradeRecords.Any())
{
numbersArray.Add(upgradeRecords.Min(x => x.Mileage));
}
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default);
if (odometerRecords.Any())
{
numbersArray.Add(odometerRecords.Min(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public int GetMinMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
var _serviceRecords = vehicleRecords.ServiceRecords.Where(x => x.Mileage != default).ToList();
if (_serviceRecords.Any())
{
numbersArray.Add(_serviceRecords.Min(x => x.Mileage));
}
var _repairRecords = vehicleRecords.CollisionRecords.Where(x => x.Mileage != default).ToList();
if (_repairRecords.Any())
{
numbersArray.Add(_repairRecords.Min(x => x.Mileage));
}
var _gasRecords = vehicleRecords.GasRecords.Where(x => x.Mileage != default).ToList();
if (_gasRecords.Any())
{
numbersArray.Add(_gasRecords.Min(x => x.Mileage));
}
var _upgradeRecords = vehicleRecords.UpgradeRecords.Where(x => x.Mileage != default).ToList();
if (_upgradeRecords.Any())
{
numbersArray.Add(_upgradeRecords.Min(x => x.Mileage));
}
var _odometerRecords = vehicleRecords.OdometerRecords.Where(x => x.Mileage != default).ToList();
if (_odometerRecords.Any())
{
numbersArray.Add(_odometerRecords.Min(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords)
{
var startDate = DateTime.Now;
var endDate = DateTime.Now;
if (!string.IsNullOrWhiteSpace(soldDate))
{
endDate = DateTime.Parse(soldDate);
}
if (!string.IsNullOrWhiteSpace(purchaseDate))
{
//if purchase date is provided, then we just have to subtract the begin date to end date and return number of months
startDate = DateTime.Parse(purchaseDate);
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
}
var dateArray = new List<DateTime>();
dateArray.AddRange(serviceRecords.Select(x => x.Date));
dateArray.AddRange(repairRecords.Select(x => x.Date));
dateArray.AddRange(gasRecords.Select(x => x.Date));
dateArray.AddRange(upgradeRecords.Select(x => x.Date));
dateArray.AddRange(odometerRecords.Select(x => x.Date));
dateArray.AddRange(taxRecords.Select(x => x.Date));
if (dateArray.Any())
{
startDate = dateArray.Min();
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
} else
{
return 1;
}
}
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage)
{
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
return results.Any(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
}
}
}

View File

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

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

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

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public List<int> ReminderRecordId { get; set; } = new List<int>();
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public CollisionRecord ToCollisionRecord() { return new CollisionRecord {
Id = Id,
VehicleId = VehicleId,

View File

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

View File

@@ -10,9 +10,18 @@
public string RedirectURL { get; set; }
public string Scope { get; set; }
public string State { get; set; }
public string CodeChallenge { get; set; }
public bool ValidateState { get; set; } = false;
public bool DisableRegularLogin { get; set; } = false;
public bool UsePKCE { get; set; } = false;
public string LogOutURL { get; set; } = "";
public string RemoteAuthURL { get { return $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; } }
public string RemoteAuthURL { get {
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
if (UsePKCE)
{
redirectUrl += $"&code_challenge={CodeChallenge}&code_challenge_method=S256";
}
return redirectUrl;
} }
}
}

View File

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

View File

@@ -9,10 +9,13 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public List<string> Tags { get; set; } = new List<string>();
}
}

View File

@@ -9,11 +9,14 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public List<string> Tags { get; set; } = new List<string>();
public ReminderRecord ToReminderRecord()
{
return new ReminderRecord
@@ -25,11 +28,14 @@
Description = Description,
Metric = Metric,
IsRecurring = IsRecurring,
UseCustomThresholds = UseCustomThresholds,
CustomThresholds = CustomThresholds,
ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval,
CustomMileageInterval = CustomMileageInterval,
CustomMonthInterval = CustomMonthInterval,
Notes = Notes
Notes = Notes,
Tags = Tags
};
}
}

View File

@@ -17,5 +17,6 @@
/// Recurring Reminders
/// </summary>
public bool IsRecurring { get; set; } = false;
public List<string> Tags { get; set; } = new List<string>();
}
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
public class ReportViewModel
{
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public List<CostForVehicleByMonth> FuelMileageForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public MPGForVehicleByMonth FuelMileageForVehicleByMonth { get; set; } = new MPGForVehicleByMonth();
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
public List<int> Years { get; set; } = new List<int>();

View File

@@ -13,5 +13,8 @@
public decimal TotalCostPerMile { get; set; }
public decimal TotalGasCostPerMile { get; set; }
public string DistanceUnit { get; set; }
public decimal TotalDepreciation { get; set; }
public decimal DepreciationPerDay { get; set; }
public decimal DepreciationPerMile { get; set; }
}
}

9
Models/SearchResult.cs Normal file
View File

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

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public List<int> ReminderRecordId { get; set; } = new List<int>();
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public ServiceRecord ToServiceRecord() { return new ServiceRecord {
Id = Id,
VehicleId = VehicleId,

View File

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

View File

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

10
Models/Sponsors.cs Normal file
View File

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

View File

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

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public List<int> ReminderRecordId { get; set; } = new List<int>();
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public string Description { get; set; }
public decimal Cost { get; set; }

12
Models/Translations.cs Normal file
View File

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

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public List<int> ReminderRecordId { get; set; } = new List<int>();
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -15,6 +15,7 @@
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public bool CopySuppliesAttachment { get; set; } = false;
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord {
Id = Id,
VehicleId = VehicleId,

View File

@@ -3,10 +3,13 @@
public class UserConfig
{
public bool UseDarkMode { get; set; }
public bool UseSystemColorMode { get; set; }
public bool EnableCsvImports { get; set; }
public bool UseMPG { get; set; }
public bool UseDescending { get; set; }
public bool EnableAuth { get; set; }
public bool DisableRegistration { get; set; }
public bool EnableRootUserOIDC { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
@@ -22,6 +25,7 @@
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
public string DefaultReminderEmail { get; set; } = string.Empty;
public string UserLanguage { get; set; } = "en_US";
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,
@@ -31,7 +35,21 @@
ImportMode.UpgradeRecord,
ImportMode.TaxRecord,
ImportMode.ReminderRecord,
ImportMode.NoteRecord};
ImportMode.NoteRecord
};
public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard;
public List<ImportMode> TabOrder { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,
ImportMode.PlanRecord,
ImportMode.OdometerRecord,
ImportMode.ServiceRecord,
ImportMode.RepairRecord,
ImportMode.UpgradeRecord,
ImportMode.GasRecord,
ImportMode.SupplyRecord,
ImportMode.TaxRecord,
ImportMode.NoteRecord,
ImportMode.ReminderRecord
};
}
}

View File

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

View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class VehicleViewModel
{
public int Id { get; set; }
public string ImageLocation { get; set; } = "/defaults/noimage.png";
public int Year { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public string LicensePlate { get; set; }
public string SoldDate { get; set; }
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public bool OdometerOptional { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
public string VehicleIdentifier { get; set; } = "LicensePlate";
//Dashboard Metric Attributes
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
public int LastReportedMileage { get; set; }
public bool HasReminders { get; set; } = false;
public decimal CostPerMile { get; set; }
public decimal TotalCost { get; set; }
public string DistanceUnit { get; set; }
}
}

View File

@@ -73,6 +73,7 @@ builder.Services.AddSingleton<ITranslationHelper, TranslationHelper>();
builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
builder.Services.AddSingleton<IUserLogic, UserLogic>();
builder.Services.AddSingleton<IOdometerLogic, OdometerLogic>();
builder.Services.AddSingleton<IVehicleLogic, VehicleLogic>();
if (!Directory.Exists("data"))
{

View File

@@ -20,38 +20,31 @@ Try it out before you download it! The live demo resets every 20 minutes.
## Download
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
Read this [Getting Started Guide](https://docs.lubelogger.com/Installation/Getting%20Started) on how to download either of them
### Docker Setup (Manual Build for Advanced Users)
1. Install Docker
2. Clone this repo
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up`
### Kubernetes Deployment
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
### Need Help?
[Documentation](https://docs.lubelogger.com/)
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
[Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
[Search Existing Issues](https://github.com/hargata/lubelog/issues)
## Dependencies
- Bootstrap
- LiteDB
- Npgsql
- Bootstrap-DatePicker
- SweetAlert2
- CsvHelper
- Chart.js
- Drawdown
- [Bootstrap](https://github.com/twbs/bootstrap)
- [LiteDB](https://github.com/mbdavid/litedb)
- [Npgsql](https://github.com/npgsql/npgsql)
- [Bootstrap-DatePicker](https://github.com/uxsolutions/bootstrap-datepicker)
- [SweetAlert2](https://github.com/sweetalert2/sweetalert2)
- [CsvHelper](https://github.com/JoshClose/CsvHelper)
- [Chart.js](https://github.com/chartjs/Chart.js)
- [Drawdown](https://github.com/adamvleggett/drawdown)
- [MailKit](https://github.com/jstedfast/MailKit)
## License
LubeLogger utilizes a dual-licensing model, see [License](/LICENSE) for more information
MIT
## Support
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
Note: Commercial users are required to maintain an active Patreon subscripton to be compliant with our licensing model.
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)

View File

@@ -1,5 +1,5 @@
@{
ViewData["Title"] = "LubeLogger API";
ViewData["Title"] = "API";
}
<div class="row">
<div class="d-flex justify-content-center">
@@ -26,7 +26,7 @@
<h6>Parameters</h6>
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -40,7 +40,87 @@
No Params
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/info</code>
</div>
<div class="col-3">
Returns details for list of vehicles or specific vehicle
</div>
<div class="col-3">
VehicleId - Id of Vehicle(optional)
</div>
</div>
<div class="row api-method">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/adjustedodometer</code>
</div>
<div class="col-3">
Returns odometer reading with adjustments applied
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
odometer - Unadjusted odometer
</div>
</div>
<div class="row api-method">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords</code>
</div>
<div class="col-3">
Returns a list of odometer records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/latest</code>
</div>
<div class="col-3">
Returns last reported odometer for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
POST
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/add</code>
</div>
<div class="col-3">
Adds Odometer Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
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 />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -54,7 +134,7 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
</div>
@@ -73,10 +153,11 @@
description - Description<br/>
cost - Cost<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 />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -90,7 +171,7 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
</div>
@@ -109,10 +190,11 @@
description - Description<br />
cost - Cost<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 />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -126,7 +208,7 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
</div>
@@ -145,10 +227,11 @@
description - Description<br />
cost - Cost<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 />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -162,7 +245,7 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
</div>
@@ -180,10 +263,11 @@
description - Description<br />
cost - Cost<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 />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -201,7 +285,7 @@
useUKMPG(bool) - Use UK Imperial Calculation
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
POST
</div>
@@ -222,10 +306,11 @@
isFillToFull(bool) - Filled To Full<br />
missedFuelUp(bool) - Missed Fuel Up<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 />
}
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -241,7 +326,7 @@
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -256,7 +341,7 @@
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
</div>
</div>
<div class="row">
<div class="row api-method">
<div class="col-1">
GET
</div>
@@ -270,58 +355,31 @@
No Params(must be root user)
</div>
</div>
<div class="row api-method">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/cleanup</code>
</div>
<div class="col-3">
Clears out temp files. Deep clean will also delete unlinked thumbnails and documents. Returns number of deleted files.
</div>
<div class="col-3">
(must be root user)<br />
deepClean(bool) - Perform deep clean
</div>
</div>
}
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords</code>
</div>
<div class="col-3">
Returns a list of odometer records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/latest</code>
</div>
<div class="col-3">
Returns last reported odometer for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5 copyable">
<code>/api/vehicle/odometerrecords/add</code>
</div>
<div class="col-3">
Adds Odometer Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
}
</div>
</div>
<script>
$('.copyable').on('click', function (e) {
copyToClipboard(e.currentTarget);
})
function showExtraFieldsInfo(){
Swal.fire({
title: "Format for Extra Fields",
html: "extrafields[i][name] - Name of Extra Field<br/>extrafields[i][value] - Value of Extra Field<br/>i is an integer that starts at 0 and increments with each extrafield",
icon: "info"
});
}
</script>

View File

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

View File

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

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

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

View File

@@ -9,10 +9,11 @@
}
@model string
@{
ViewData["Title"] = "LubeLogger";
ViewData["Title"] = "Garage";
}
@section Scripts {
<script src="~/js/garage.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/js/settings.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/js/supplyrecord.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
}
@@ -41,7 +42,12 @@
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</span></a>
</li>
}
@if (!User.IsInRole(nameof(UserData.IsRootUser)))
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<li>
<button class="nav-link" onclick="showRootAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button>
</li>
} else
{
<li>
<button class="nav-link" onclick="showAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button>
@@ -58,31 +64,31 @@
<div class="d-flex lubelogger-navbar">
<img src="@logoUrl" />
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></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 @(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 me-2"></i>@translator.Translate(userLanguage,"Garage")</button>
<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" 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 me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
<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" 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 me-2"></i>@translator.Translate(userLanguage, "Calendar")</button>
<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 @(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 me-2"></i>@translator.Translate(userLanguage,"Settings")</button>
<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 dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person me-2"></i>@User.Identity.Name</a>
<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)))
{
@@ -90,7 +96,12 @@
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</a>
</li>
}
@if (!User.IsInRole(nameof(UserData.IsRootUser)))
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<li>
<button class="dropdown-item" onclick="showRootAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
} else
{
<li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>

View File

@@ -7,18 +7,23 @@
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="addVehicleModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
<h5 class="modal-title" id="updateAccountModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="modal-body" onkeydown="handleEnter(this)">
<form class="form-inline">
<div class="form-group">
<label for="inputUsername">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Account Username")" value="@Model.UserName">
<label for="inputEmail">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control" placeholder="@translator.Translate(userLanguage, "Email Address")" value="@Model.EmailAddress">
<label for="inputPassword">@translator.Translate(userLanguage, "New Password")</label>
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "New Password")" value="">
<label for="inputPassword">@translator.Translate(userLanguage, "New Password")</label>
<div class="input-group">
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "New Password")" value="">
<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>
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Token")" value="">
<div class="row">

View File

@@ -73,7 +73,7 @@
Swal.fire({
title: 'Field Name',
html: `
<input type="text" id="inputExtraFieldName" class="swal2-input" placeholder="Field Name">
<input type="text" id="inputExtraFieldName" class="swal2-input" placeholder="Field Name" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Add',
focusConfirm: false,

View File

@@ -1,7 +1,7 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<Vehicle>
@model List<VehicleViewModel>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
@@ -26,28 +26,63 @@
}
<div class="row">
<div class="row gy-3 align-items-stretch vehiclesContainer">
@foreach (Vehicle vehicle in Model)
@foreach (VehicleViewModel vehicle in Model)
{
@if (!(userConfig.HideSoldVehicles && !string.IsNullOrWhiteSpace(vehicle.SoldDate)))
{
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none garage-item" ondragover="dragOver(event)" ondrop="dropBox(event, @vehicle.Id)" draggable="true" ondragstart="dragStart(event, @vehicle.Id)" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="@await Html.PartialAsync("_VehicleExtraFields", vehicle.ExtraFields)" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-4 user-select-none garage-item" ondragover="dragOver(event)" ondrop="dropBox(event, @vehicle.Id)" draggable="true" ondragstart="dragStart(event, @vehicle.Id)" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="@await Html.PartialAsync("_VehicleExtraFields", vehicle.ExtraFields)" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
<div class="card" onclick="viewVehicle(@vehicle.Id)">
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down; pointer-events:none; @(string.IsNullOrWhiteSpace(vehicle.SoldDate) ? "" : "filter: grayscale(100%);")" />
@if (!string.IsNullOrWhiteSpace(vehicle.SoldDate))
{
<div class="vehicle-sold-banner"><p class='display-6 mb-0'>@translator.Translate(userLanguage, "SOLD")</p></div>
} else if (vehicle.DashboardMetrics.Any())
{
<div class="vehicle-sold-banner">
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.Default) && vehicle.LastReportedMileage != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-speedometer me-2"></i>@vehicle.LastReportedMileage.ToString("N0")</span>
</div>
@if (vehicle.HasReminders)
{
<div>
<span class="me-2"><i class="bi bi bi-bell-fill text-warning"></i></span>
</div>
}
</div>
}
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.CostPerMile) && vehicle.CostPerMile != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.CostPerMile.ToString("C2")}/{vehicle.DistanceUnit}")</span>
</div>
</div>
}
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.TotalCost) && vehicle.TotalCost != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.TotalCost.ToString("C2")}")</span>
</div>
</div>
}
</div>
}
<div class="card-body">
<h5 class="card-title text-truncate garage-item-year" data-unit="@vehicle.Year">@($"{vehicle.Year}")</h5>
<h5 class="card-title text-truncate">@($"{vehicle.Make}")</h5>
<h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5>
<p class="card-text text-truncate">@vehicle.LicensePlate</p>
<p class="card-text text-truncate">@StaticHelper.GetVehicleIdentifier(vehicle)</p>
</div>
</div>
</div>
}
}
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 garage-item-add">
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-4 garage-item-add">
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
</div>

View File

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

View File

@@ -13,10 +13,14 @@
<div class="col-12 col-md-6">
<input id="preferredGasUnit" style="display:none;" value="@Model.UserConfig.PreferredGasUnit" />
<input id="preferredFuelMileageUnit" style="display:none;" value="@Model.UserConfig.PreferredGasMileageUnit" />
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
<label class="form-check-label" for="enableDarkMode">@translator.Translate(userLanguage, "Dark Mode")</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="useSystemColorMode" checked="@Model.UserConfig.UseSystemColorMode">
<label class="form-check-label" for="useSystemColorMode">@translator.Translate(userLanguage, "Adaptive Color Mode")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableCsvImports" checked="@Model.UserConfig.EnableCsvImports">
<label class="form-check-label" for="enableCsvImports">@translator.Translate(userLanguage, "Enable CSV Imports")</label>
@@ -67,16 +71,34 @@
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="form-check form-switch">
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.UserConfig.EnableAuth">
<label class="form-check-label" for="enableAuth">@translator.Translate(userLanguage, "Enable Authentication")</label>
</div>
@if (Model.UserConfig.EnableAuth)
{
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="disableRegistration" checked="@Model.UserConfig.DisableRegistration">
<label class="form-check-label" for="disableRegistration">@translator.Translate(userLanguage, "Disable Registration")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableRootUserOIDC" checked="@Model.UserConfig.EnableRootUserOIDC">
<label class="form-check-label" for="enableRootUserOIDC">@translator.Translate(userLanguage, "Enable OIDC for Root User")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Uses the Default Reminder Email for OIDC Auth")</small></label>
</div>
}
}
</div>
<div class="col-12 col-md-6">
<div class="row" id="visibleTabs">
<div class="col-12">
<span class="lead">@translator.Translate(userLanguage, "Visible Tabs")</span>
<div class="row">
<div class="col-11">
<span class="lead">@translator.Translate(userLanguage, "Visible Tabs")</span>
</div>
<div class="col-1">
<button onclick="showTabReorderModal()" class="btn text-secondary btn-sm"><i class="bi bi-arrow-down-up"></i></button>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<ul class="list-group">
@@ -150,12 +172,28 @@
</div>
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
}
</select>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="input-group">
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option value="@uiLanguage" @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@StaticHelper.GetTranslationName(uiLanguage)</!option>
}
</select>
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="showTranslationEditor()"><i class="bi bi-pencil"></i></button>
</div>
</div>
} else
{
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option value="@uiLanguage" @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@StaticHelper.GetTranslationName(uiLanguage)</!option>
}
</select>
}
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
@@ -165,11 +203,11 @@
<span class="lead">@translator.Translate(userLanguage, "Backups")</span>
<div class="row">
<div class="col-6 d-grid">
<button onclick="makeBackup()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Create")</button>
<button onclick="makeBackup()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Create")</button>
</div>
<div class="col-6 d-grid">
<input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup">
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">@translator.Translate(userLanguage, "Restore")</button>
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md text-truncate">@translator.Translate(userLanguage, "Restore")</button>
</div>
</div>
</div>
@@ -178,23 +216,45 @@
<div class="row">
<div class="col-6 d-grid">
<input onChange="uploadLanguage(this)" type="file" accept=".json" class="d-none" id="inputLanguage">
<button onclick="openUploadLanguage()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Upload")</button>
<div class="btn-group">
<button onclick="openUploadLanguage()" class="btn btn-primary btn-md text-truncate"><i class="bi bi-upload"></i><span class="ms-2 d-sm-inline d-md-none d-xl-inline">@translator.Translate(userLanguage, "Upload")</span></button>
<button type="button" class="btn btn-md btn-primary btn-md dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showTranslationDownloader()">@translator.Translate(userLanguage, "Get Translations")</a></li>
</ul>
</div>
</div>
<div class="col-6 d-grid">
<button onclick="deleteLanguage()" @(Model.UserConfig.UserLanguage == "en_US" ? "disabled" : "") class="btn btn-danger btn-md">@translator.Translate(userLanguage, "Delete")</button>
<button onclick="deleteLanguage()" @(Model.UserConfig.UserLanguage == "en_US" ? "disabled" : "") class="btn btn-danger btn-md text-truncate"><i class="bi bi-trash"></i><span class="ms-2 d-sm-inline d-md-none d-xl-inline">@translator.Translate(userLanguage, "Delete")</span></button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Server-wide Settings")</span>
<span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span>
<div class="row">
<div class="col-6 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Extra Fields")</button>
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button>
</div>
<div class="col-6 d-grid">
<button onclick="showReminderUrgencyThresholdModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Reminders")</button>
<button onclick="showReminderUrgencyThresholdModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Reminders")</button>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<span class="lead text-wrap">@translator.Translate(userLanguage, "Default Reminder Email")</span>
<div class="row">
<div class="col-12 d-grid">
<div class="input-group">
<input id="inputDefaultEmail" class="form-control" placeholder="@translator.Translate(userLanguage,"Default Email for Reminder")" value="@Model.UserConfig.DefaultReminderEmail" onkeydown="handleDefaultReminderInputKeyDown()">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="updateSettings()"><i class="bi bi-floppy"></i></button>
</div>
</div>
</div>
</div>
</div>
@@ -219,7 +279,7 @@
</p>
<p class="lead">
If you enjoyed using this app, please consider spreading the good word.<br />
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
If you want to support the development of this project, consider subscribing to <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
</p>
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
@@ -247,15 +307,63 @@
<li class="list-group-item">CsvHelper</li>
<li class="list-group-item">Chart.js</li>
<li class="list-group-item">Drawdown</li>
<li class="list-group-item">MailKit</li>
</ul>
</div>
</div>
<div class="row" id="sponsorsContainer"></div>
<div class="modal fade" data-bs-focus="false" id="extraFieldModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="extraFieldModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="translationEditorModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="translationEditorModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="translationDownloadModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="translationDownloadModalContent">
</div>
</div>
</div>
<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-content" id="tabReorderModalContent">
<div class="modal-header">
<h5 class="modal-title" id="translationEditorModalLabel">@translator.Translate(userLanguage, "Reorder Tabs")</h5>
<button type="button" class="btn-close" onclick="hideTabReorderModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12">
<ul class="list-group lubelog-tab-groups">
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)" draggable="true" data-tab="@ImportMode.Dashboard">@translator.Translate(userLanguage, "Dashboard")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)" draggable="true" data-tab="@ImportMode.PlanRecord">@translator.Translate(userLanguage, "Planner")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)" draggable="true" data-tab="@ImportMode.OdometerRecord">@translator.Translate(userLanguage, "Odometer")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)" draggable="true" data-tab="@ImportMode.ServiceRecord">@translator.Translate(userLanguage, "Service Records")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)" draggable="true" data-tab="@ImportMode.RepairRecord">@translator.Translate(userLanguage, "Repairs")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)" draggable="true" data-tab="@ImportMode.UpgradeRecord">@translator.Translate(userLanguage, "Upgrades")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)" draggable="true" data-tab="@ImportMode.GasRecord">@translator.Translate(userLanguage, "Fuel")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)" draggable="true" data-tab="@ImportMode.SupplyRecord">@translator.Translate(userLanguage, "Supplies")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)" draggable="true" data-tab="@ImportMode.TaxRecord">@translator.Translate(userLanguage, "Taxes")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)" draggable="true" data-tab="@ImportMode.NoteRecord">@translator.Translate(userLanguage, "Notes")</li>
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)" draggable="true" data-tab="@ImportMode.ReminderRecord">@translator.Translate(userLanguage, "Reminder")</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger me-auto" onclick="resetTabOrder()">@translator.Translate(userLanguage, "Reset Tab Order")</button>
<button type="button" class="btn btn-secondary" onclick="hideTabReorderModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" class="btn btn-primary" onclick="updateSettings()">@translator.Translate(userLanguage, "Save Tab Order")</button>
</div>
</div>
</div>
</div>
<script>
function showReminderUrgencyThresholdModal(){
Swal.fire({
@@ -304,137 +412,6 @@
}
});
}
function showExtraFieldModal() {
$.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) {
$("#extraFieldModalContent").html(data);
$("#extraFieldModal").modal('show');
});
}
function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide');
}
function getCheckedTabs() {
var visibleTabs = $("#visibleTabs :checked").map(function () {
return this.value;
});
return visibleTabs.toArray();
}
function deleteLanguage() {
var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`;
$.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) {
//reset user language back to en_US
$("#defaultLanguage").val('en_US');
updateSettings();
});
}
function updateSettings() {
var visibleTabs = getCheckedTabs();
var defaultTab = $("#defaultTab").val();
if (!visibleTabs.includes(defaultTab)) {
defaultTab = "Dashboard"; //default to dashboard.
}
var userConfigObject = {
useDarkMode: $("#enableDarkMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
userLanguage: $("#defaultLanguage").val(),
visibleTabs: visibleTabs,
defaultTab: defaultTab
}
sloader.show();
$.post('/Home/WriteToSettings', { userConfig: userConfigObject }, function (data) {
sloader.hide();
if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(genericErrorMessage());
}
})
}
function makeBackup() {
$.get('/Files/MakeBackup', function (data) {
window.location.href = data;
});
}
function openUploadLanguage() {
$("#inputLanguage").click();
}
function openRestoreBackup() {
$("#inputBackup").click();
}
function uploadLanguage(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
$.ajax({
url: "/Files/HandleTranslationFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
sloader.hide();
if (response.success) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(response.message);
}
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function restoreBackup(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
console.log('LubeLogger - DB Restoration Started');
sloader.show();
$.ajax({
url: "/Files/HandleFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
if (response.trim() != '') {
$.post('/Files/RestoreBackup', { fileName: response }, function (data) {
sloader.hide();
if (data) {
console.log('LubeLogger - DB Restoration Completed');
successToast("Backup Restored");
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
} else {
errorToast(genericErrorMessage());
console.log('LubeLogger - DB Restoration Failed - Failed to process backup file.');
}
});
} else {
console.log('LubeLogger - DB Restoration Failed - Failed to upload backup file.');
}
},
error: function () {
sloader.hide();
console.log('LubeLogger - DB Restoration Failed - Request failed to reach backend, please check file size.');
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function enableAuthCheckChanged() {
var enableAuth = $("#enableAuth").is(":checked");
if (enableAuth) {
@@ -443,7 +420,7 @@
title: 'Setup Credentials',
html: `
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Setup',
focusConfirm: false,
@@ -476,4 +453,5 @@
});
}
}
loadSponsors();
</script>

View File

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

View File

@@ -0,0 +1,39 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model Dictionary<string, string>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="translationEditorModalLabel">@translator.Translate(userLanguage, "Translation Editor")</h5>
<button type="button" class="btn-close" onclick="hideTranslationEditor()" aria-label="Close"></button>
</div>
<div class="modal-body" onkeydown="handleEnter(this)">
<form class="form-inline">
<div class="form-group" style="max-height:50vh; overflow-x:hidden; overflow-y:scroll;">
@foreach(var translation in Model)
{
<div class="row translation-keyvalue mb-2">
<div class="col-md-6 col-12 translation-key">@translation.Key.Replace("_", " ")</div>
<div class="col-md-6 col-12 translation-value">
<textarea style="height:100%;width:98%;" class="form-control" placeholder="@translation.Value">@translation.Value</textarea>
</div>
</div>
}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideTranslationEditor()">@translator.Translate(userLanguage, "Cancel")</button>
<div class="btn-group">
<button type="button" onclick="saveTranslation()" class="btn btn-primary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Save Translation")</button>
<button type="button" class="btn btn-md btn-primary btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportTranslation()">@translator.Translate(userLanguage, "Export Translation")</a></li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,76 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model Translations
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="translationDownloaderModalLabel">@translator.Translate(userLanguage, "Available Translations")</h5>
<button type="button" class="btn-close" onclick="hideTranslationDownloader()" aria-label="Close"></button>
</div>
<div class="modal-body" onkeydown="handleEnter(this)">
<form class="form-inline">
<div class="form-group" style="max-height:50vh; overflow-x:hidden; overflow-y:scroll;">
@foreach(var translation in Model.Africa)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Africa','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
@foreach (var translation in Model.Asia)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Asia','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
@foreach (var translation in Model.Europe)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Europe','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
@foreach (var translation in Model.NorthAmerica)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('NorthAmerica','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
@foreach (var translation in Model.SouthAmerica)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('SouthAmerica','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
@foreach (var translation in Model.Oceania)
{
<div class="row mb-2">
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
<div class="col-2">
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Oceania','@translation')"><i class="bi bi-download"></i></button>
</div>
</div>
}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideTranslationDownloader()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" class="btn btn-primary" onclick="downloadAllTranslations()">@translator.Translate(userLanguage, "Download All Translations")</button>
</div>

View File

@@ -6,14 +6,14 @@
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Login";
ViewData["Title"] = "Forgot Password";
}
@section Scripts {
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<div>
<div style="max-width:204px;">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>

View File

@@ -5,25 +5,31 @@
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
var registrationDisabled = config.GetServerDisabledRegistration();
var openIdConfigName = config.GetOpenIDConfig().Name;
}
@{
ViewData["Title"] = "LubeLogger - Login";
ViewData["Title"] = "Login";
}
@section Scripts {
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<div>
<div style="max-width:204px;">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control">
</div>
<div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<div class="input-group">
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
<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 class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputPersistent">
@@ -46,9 +52,12 @@
<div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
</div>
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
</div>
@if (!registrationDisabled)
{
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
</div>
}
</div>
</div>
</div>

View File

@@ -7,14 +7,14 @@
}
@model string
@{
ViewData["Title"] = "LubeLogger - Register";
ViewData["Title"] = "Register";
}
@section Scripts {
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<div>
<div style="max-width:204px;">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>

View File

@@ -6,14 +6,14 @@
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Register";
ViewData["Title"] = "Register";
}
@section Scripts {
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<div>
<div style="max-width:204px;">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
@@ -29,7 +29,12 @@
</div>
<div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<input type="password" id="inputUserPassword" class="form-control">
<div class="input-group">
<input type="password" id="inputUserPassword" class="form-control">
<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 class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>

View File

@@ -6,14 +6,14 @@
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Register";
ViewData["Title"] = "Reset Password";
}
@section Scripts {
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<div>
<div style="max-width:204px;">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
@@ -25,7 +25,12 @@
</div>
<div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label>
<input type="password" id="inputUserPassword" class="form-control">
<div class="input-group">
<input type="password" id="inputUserPassword" class="form-control">
<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 class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Reset Password")</button>

View File

@@ -1,6 +1,6 @@
@using CarCareTracker.Helper
@{
ViewData["Title"] = "Admin";
ViewData["Title"] = "Database Migration";
}
@inject IConfiguration config;
@inject ITranslationHelper translator
@@ -83,6 +83,6 @@
});
}
function importToPostgres() {
$("#inputImport").click();
$("#inputImport").trigger('click');
}
</script>

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