Compare commits

...

168 Commits

Author SHA1 Message Date
Hargata Softworks
920de489be Merge pull request #372 from hargata/Hargata/delta.stats
Added MOTD.
2024-03-07 08:46:14 -07:00
DESKTOP-GENO133\IvanPlex
19cb964543 Added MOTD. 2024-03-07 08:44:58 -07:00
Hargata Softworks
eb03979ad8 Merge pull request #369 from hargata/Hargata/delta.stats
Added Total and Average Cost
2024-03-06 07:44:28 -07:00
DESKTOP-GENO133\IvanPlex
ebe871b82a reduced operations 2024-03-06 07:42:46 -07:00
DESKTOP-GENO133\IvanPlex
04a103fdc0 column specific search 2024-03-06 07:40:18 -07:00
DESKTOP-GENO133\IvanPlex
d0f80d4150 Added Total and Average Cost 2024-03-05 08:43:10 -07:00
Hargata Softworks
48c388dea7 Merge pull request #368 from redge76/Sample_postgreSQL
Sample postgreSQL configuration
2024-03-04 17:13:02 -07:00
Redge
d05f8b1f84 Sample postgrSQL connection stringin file .env
add a sample connection string to enable postgrSQL database backend
2024-03-05 01:09:10 +01:00
Redge
167d6e607e Sample docker-compose.postgresql.yml 2024-03-05 01:05:24 +01:00
Hargata Softworks
8755c6379a Merge pull request #367 from hargata/Hargata/record.delta.stats
Hargata/record.delta.stats
2024-03-04 16:20:24 -07:00
DESKTOP-GENO133\IvanPlex
1e2bd0ea4b Allow users to set their own reminder urgency threshold 2024-03-04 16:18:09 -07:00
DESKTOP-GENO133\IvanPlex
2cf93cf669 Added setting to hide sold vehicles. 2024-03-04 13:37:07 -07:00
Hargata Softworks
a18b81d52e Merge pull request #363 from hargata/Hargata/record.delta.stats
Hargata/record.delta.stats
2024-03-03 21:10:14 -07:00
DESKTOP-GENO133\IvanPlex
604c98a031 allowed users to configure their own file extensions. 2024-03-03 19:32:20 -07:00
DESKTOP-GENO133\IvanPlex
940f48b816 Updated version number 2024-03-03 09:20:02 -07:00
DESKTOP-GENO133\IvanPlex
f81545178d Added statistics between records. 2024-03-03 09:09:42 -07:00
Hargata Softworks
a87b599bdf Merge pull request #362 from hargata/Hargata/persist.columns
null check for column preferences
2024-03-02 19:13:11 -07:00
DESKTOP-GENO133\IvanPlex
2c45521fb4 null check for column preferences 2024-03-02 19:11:21 -07:00
Hargata Softworks
0870387995 Merge pull request #361 from hargata/Hargata/persist.columns
Allows users to inject root user credentials as environment variables.
2024-03-02 07:53:48 -07:00
DESKTOP-GENO133\IvanPlex
be281c78ff Allows users to inject root user credentials as environment variables. 2024-03-02 07:51:37 -07:00
Hargata Softworks
87c54335db Merge pull request #360 from hargata/Hargata/persist.columns
authenticate root user via configHelper
2024-03-02 07:13:43 -07:00
DESKTOP-GENO133\IvanPlex
12e6c1677b null checks 2024-03-02 07:13:13 -07:00
DESKTOP-GENO133\IvanPlex
f69b789346 authenticate root user via configHelper 2024-03-02 07:11:54 -07:00
Hargata Softworks
9f05295f18 Merge pull request #359 from hargata/Hargata/persist.columns
revamped column check toggle behavior
2024-03-02 06:50:52 -07:00
DESKTOP-GENO133\IvanPlex
bf14e4c8c0 added functonality to persist column visibility 2024-03-02 06:49:40 -07:00
Hargata Softworks
bd4db09637 Merge pull request #357 from hargata/Hargata/responsive.table.columns
allow table columns to grow as needed.
2024-03-02 05:48:23 -07:00
DESKTOP-GENO133\IvanPlex
4b5dcb1c7e revamped column check toggle behavior 2024-03-02 05:47:22 -07:00
DESKTOP-GENO133\IvanPlex
b7f1ac5e8f allow table columns to grow as needed. 2024-03-02 04:57:45 -07:00
Hargata Softworks
bcf3c3dc33 Merge pull request #354 from hargata/Hargata/gas.search
deprecated pinned notes
2024-03-01 04:34:18 -07:00
DESKTOP-GENO133\IvanPlex
993c271fa8 deprecated pinned notes 2024-03-01 04:31:13 -07:00
Hargata Softworks
b8b545a072 Merge pull request #353 from hargata/Hargata/gas.search
Added Search Functionality to Gas Tab
2024-03-01 04:17:19 -07:00
DESKTOP-GENO133\IvanPlex
84b748a361 added more data points to vehicle history report. 2024-03-01 04:14:53 -07:00
DESKTOP-GENO133\IvanPlex
ef2b8cadc7 added notes as a toggleable column 2024-02-29 19:06:56 -07:00
DESKTOP-GENO133\IvanPlex
d8cfa2c397 Added gas search 2024-02-29 19:01:44 -07:00
Hargata Softworks
d8c7d63f21 Merge pull request #352 from hargata/Hargata/vehicle.extra.fields
Added Additional Fields for Vehicle
2024-02-29 17:20:50 -07:00
DESKTOP-GENO133\IvanPlex
3d3aa23a65 added sold and purchased date to vehicle. 2024-02-29 17:20:03 -07:00
DESKTOP-GENO133\IvanPlex
6993fe5df8 prevent overlap of additional fields on vehicle level vs record level. 2024-02-29 16:38:31 -07:00
Hargata Softworks
48d80a8460 Merge pull request #350 from hargata/Hargata/universal.search
Updated translation
2024-02-29 14:10:34 -07:00
DESKTOP-GENO133\IvanPlex
742c5b3489 Updated translation 2024-02-29 14:09:06 -07:00
Hargata Softworks
3cd75c02ee Merge pull request #349 from hargata/Hargata/universal.search
Added Search Functionality
2024-02-29 13:57:55 -07:00
DESKTOP-GENO133\IvanPlex
854d5f0afe added search functionality to other tabs 2024-02-29 13:56:24 -07:00
DESKTOP-GENO133\IvanPlex
daabbfa759 Fixed null reference when no rows matches keyword. 2024-02-29 13:48:25 -07:00
DESKTOP-GENO133\IvanPlex
42ca4f76bf added a universal table search function 2024-02-29 13:31:32 -07:00
Hargata Softworks
6bf2801b1e Merge pull request #348 from hargata/Hargata/show.extrafields
Show Extra Fields as Columns in tables.
2024-02-29 12:18:13 -07:00
DESKTOP-GENO133\IvanPlex
3be3a28cc9 moved js function to shared file so that it will work with shop supplies 2024-02-29 12:17:26 -07:00
DESKTOP-GENO133\IvanPlex
916e1640de added extra fields functionality to supplies 2024-02-29 12:15:37 -07:00
DESKTOP-GENO133\IvanPlex
ecb3b74581 added extra fields to odometer records 2024-02-29 11:59:55 -07:00
DESKTOP-GENO133\IvanPlex
2171c7fbe3 added extra fields functionality to tax 2024-02-29 11:53:05 -07:00
DESKTOP-GENO133\IvanPlex
4a909e5c44 Added extra fields functionality to fuel tab. 2024-02-29 11:36:54 -07:00
DESKTOP-GENO133\IvanPlex
c44c239b35 Added Extra Fields ability to Upgrades 2024-02-29 10:56:03 -07:00
DESKTOP-GENO133\IvanPlex
6990046a8d Added Extra Field Columns to Repair Records 2024-02-29 10:49:46 -07:00
DESKTOP-GENO133\IvanPlex
0750b080f6 added setting to hide extra field columns 2024-02-29 10:34:24 -07:00
DESKTOP-GENO133\IvanPlex
ca09d2ca66 more stuff 2024-02-29 10:18:56 -07:00
DESKTOP-GENO133\IvanPlex
ed3749eaf2 proof of concept for toggling column visibilities 2024-02-29 10:09:58 -07:00
DESKTOP-GENO133\IvanPlex
fa3c391ee9 added ability to show extra fields and their values. 2024-02-28 21:21:45 -07:00
Hargata Softworks
c6d945b8c0 Merge pull request #347 from hargata/Hargata/max.odo.api
Added API Endpoint to retrieve last reported odometer reading.
2024-02-28 20:13:18 -07:00
DESKTOP-GENO133\IvanPlex
9dc20abbb4 Added API Endpoint to retrieve last reported odometer reading. 2024-02-28 20:12:01 -07:00
Hargata Softworks
94cd543e56 Merge pull request #346 from hargata/Hargata/user.update
Added functionality for users to update their account information after signing up.
2024-02-28 16:31:16 -07:00
DESKTOP-GENO133\IvanPlex
4f50939eb2 Updated version number 2024-02-28 16:29:46 -07:00
DESKTOP-GENO133\IvanPlex
60f792b2ef added link in mobile nav 2024-02-28 16:29:19 -07:00
DESKTOP-GENO133\IvanPlex
383945b156 Updated translation file 2024-02-28 16:26:36 -07:00
DESKTOP-GENO133\IvanPlex
0e960715ee Delete token only if user save is successful and log user out after updating profile. 2024-02-28 16:20:01 -07:00
DESKTOP-GENO133\IvanPlex
522fd2a9f5 Added empty email address for root users. everything should be functional from here on out. 2024-02-28 15:46:59 -07:00
DESKTOP-GENO133\IvanPlex
9091b3d2a8 added frontend method to retrieve and send token to user's email address. 2024-02-28 15:02:54 -07:00
DESKTOP-GENO133\IvanPlex
acc3d2f6d0 Added email helper method to send token for user account update. 2024-02-28 14:33:10 -07:00
DESKTOP-GENO133\IvanPlex
a9d7ab0193 Added logging to OIDC Login flow. 2024-02-28 14:22:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cd720c34dd Updated logic 2024-02-28 14:18:12 -07:00
DESKTOP-GENO133\IvanPlex
c3839b1e98 added logic for updating user details. 2024-02-25 19:37:15 -07:00
Hargata Softworks
085eb2a9a0 Merge pull request #340 from hargata/Hargata/user.suggested.improvement
Bug Fixes
2024-02-24 16:57:17 -07:00
DESKTOP-GENO133\IvanPlex
f20c04d523 fixed bug in linux where oncontextmenu is fired before onrightclick. 2024-02-24 16:54:42 -07:00
DESKTOP-GENO133\IvanPlex
b7b7d6ad3e fixed tags input on android device. 2024-02-24 16:39:26 -07:00
Hargata Softworks
299444d767 Merge pull request #338 from hargata/Hargata/user.suggested.improvement
Updated version number
2024-02-24 11:42:41 -07:00
DESKTOP-GENO133\IvanPlex
0dbaa68fc0 added copy and paste support for tags input 2024-02-24 11:41:16 -07:00
DESKTOP-GENO133\IvanPlex
80fd9e136a Updated version number 2024-02-24 11:10:28 -07:00
Hargata Softworks
ba27db5f75 Merge pull request #337 from hargata/Hargata/user.suggested.improvement
added help links
2024-02-24 10:26:41 -07:00
Hargata Softworks
4629271fb2 Update issue templates 2024-02-24 10:24:55 -07:00
DESKTOP-GENO133\IvanPlex
91177af98b added help links 2024-02-24 10:19:15 -07:00
Hargata Softworks
26281d1cbb Merge pull request #336 from hargata/Hargata/user.suggested.improvement
added OIDC State validation
2024-02-24 09:53:12 -07:00
DESKTOP-GENO133\IvanPlex
a80c6e12ad added OIDC State validation 2024-02-24 06:14:44 -07:00
DESKTOP-GENO133\IvanPlex
8e208e8791 updated colors. 2024-02-22 22:43:51 -07:00
DESKTOP-GENO133\IvanPlex
6e87c2d5cc Updated website and re-organized readme 2024-02-22 22:36:56 -07:00
Hargata Softworks
3d96984735 Update README.md 2024-02-22 20:06:43 -07:00
Hargata Softworks
4626fdf04a Merge pull request #330 from hargata/Hargata/minor.improvement
added state for OIDC
2024-02-22 16:04:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a2c013ec43 added state for OIDC 2024-02-22 15:59:35 -07:00
DESKTOP-GENO133\IvanPlex
9262131936 updated translation file. 2024-02-22 11:46:28 -07:00
Hargata Softworks
02c5302653 Update README.md 2024-02-22 11:20:19 -07:00
Hargata Softworks
fa73b61e3f Merge pull request #328 from hargata/Hargata/generic.editor
added functions for pinning notes.
2024-02-22 10:48:22 -07:00
DESKTOP-GENO133\IvanPlex
96ade10289 added functions for pinning notes. 2024-02-22 10:46:18 -07:00
Hargata Softworks
acf16a6c28 Merge pull request #327 from hargata/Hargata/generic.editor
added clear overrides
2024-02-22 10:12:13 -07:00
DESKTOP-GENO133\IvanPlex
dedf8fc2f7 added method to replenish supplies. 2024-02-22 10:11:26 -07:00
DESKTOP-GENO133\IvanPlex
7099652083 added clear overrides 2024-02-22 08:56:31 -07:00
Hargata Softworks
d0f9795e63 Merge pull request #326 from hargata/Hargata/clean.up.usings
code cleanup.
2024-02-21 22:58:00 -07:00
DESKTOP-GENO133\IvanPlex
0ffc469ac8 fixed missing end span tag. fixed deprecated js function. 2024-02-21 22:57:16 -07:00
DESKTOP-GENO133\IvanPlex
b5ea3c63a1 code cleanup. 2024-02-21 22:48:37 -07:00
Hargata Softworks
5901531ddf Merge pull request #325 from hargata/Hargata/bulk.selection
Hargata/bulk.selection
2024-02-21 22:33:19 -07:00
DESKTOP-GENO133\IvanPlex
814aa8d22b obsolete using 2024-02-21 22:30:25 -07:00
DESKTOP-GENO133\IvanPlex
d3aa55f987 Updated version number. 2024-02-21 22:29:47 -07:00
DESKTOP-GENO133\IvanPlex
8b7fb7ef0f added edit functionality for services, repairs, and upgrades 2024-02-21 22:28:25 -07:00
DESKTOP-GENO133\IvanPlex
0fab57d19c added bulk edit functionality. 2024-02-21 21:39:39 -07:00
DESKTOP-GENO133\IvanPlex
0d8822c496 reorganized folders. 2024-02-21 20:54:14 -07:00
Hargata Softworks
7235931f38 Merge pull request #322 from hargata/Hargata/bulk.selection
Bulk Operations.
2024-02-21 16:49:41 -07:00
DESKTOP-GENO133\IvanPlex
410f45c6c2 ssssss 2024-02-21 16:22:12 -07:00
DESKTOP-GENO133\IvanPlex
9a44d80be4 fixed compatibility on hybrid device. 2024-02-21 15:51:38 -07:00
DESKTOP-GENO133\IvanPlex
2f52ed64ba standardize experience across mobile devices 2024-02-21 15:05:19 -07:00
DESKTOP-GENO133\IvanPlex
dd68dec05c moved stuff around and added duplicate option. 2024-02-21 13:45:08 -07:00
DESKTOP-GENO133\IvanPlex
a8bc9f90e1 display number of records manipulated. 2024-02-21 12:57:09 -07:00
DESKTOP-GENO133\IvanPlex
8759216dd6 only select rows that are visible. 2024-02-21 12:50:11 -07:00
DESKTOP-GENO133\IvanPlex
0851e0a222 made things mobile friendly. 2024-02-21 12:38:50 -07:00
DESKTOP-GENO133\IvanPlex
40f1f1380e added select all to context menu for mobile friendliness. 2024-02-21 11:53:00 -07:00
DESKTOP-GENO133\IvanPlex
2a5ff7a911 added ability to select reminders from tax modal. 2024-02-21 10:32:25 -07:00
DESKTOP-GENO133\IvanPlex
0590f991d2 added shared method to push back recurring reminders. 2024-02-21 10:10:28 -07:00
DESKTOP-GENO133\IvanPlex
c6a96df3e9 updated readme 2024-02-20 21:21:20 -07:00
DESKTOP-GENO133\IvanPlex
0088c74b20 added ability to select rows when ctrl key is held. 2024-02-20 20:23:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6af0d8b88e added functionality to delete records in bulk. 2024-02-19 22:32:07 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5204a71b00 added option to bulk move records across service, upgrade, and repair. 2024-02-19 21:57:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8b2866b89b added ability to select bulk data. 2024-02-19 16:48:12 -07:00
Hargata Softworks
65f638f336 Merge pull request #319 from hargata/Hargata/csv.odo.insert
Hargata/csv.odo.insert
2024-02-19 14:00:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f92e95e01c Merge branch 'main' into Hargata/report.usability 2024-02-19 13:59:04 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2aeae70060 Enabled Auto Odometer CSV when importing via CSV. 2024-02-19 13:58:23 -07:00
Hargata Softworks
98b1cd9bc5 Merge pull request #315 from hargata/Hargata/linked.planner.to.reminder
check if reminder still exists.
2024-02-18 15:04:45 -07:00
DESKTOP-GENO133\IvanPlex
a02684f921 check if reminder still exists. 2024-02-18 15:04:02 -07:00
Hargata Softworks
13ddc6309e Merge pull request #314 from hargata/Hargata/linked.planner.to.reminder
Updated version number
2024-02-18 14:42:42 -07:00
DESKTOP-GENO133\IvanPlex
2b2fd3ae28 Updated version number 2024-02-18 14:42:05 -07:00
Hargata Softworks
2dd3dc1718 Merge pull request #313 from hargata/Hargata/linked.planner.to.reminder
added ability to create plans from reminders.
2024-02-18 14:23:39 -07:00
DESKTOP-GENO133\IvanPlex
fe1b96d58c updated translation 2024-02-18 14:22:43 -07:00
DESKTOP-GENO133\IvanPlex
8dfee0fd12 added ability to create plans from reminders. 2024-02-18 14:20:20 -07:00
Hargata Softworks
275b60aa14 Merge pull request #310 from hargata/Hargata/supply.usage.history
Supply Usage History
2024-02-18 13:16:46 -07:00
DESKTOP-GENO133\IvanPlex
dd14b71e68 added front end to show used supplies. 2024-02-18 11:32:04 -07:00
DESKTOP-GENO133\IvanPlex
849171c4df moved function to shared js. 2024-02-17 21:43:19 -07:00
DESKTOP-GENO133\IvanPlex
2183c77606 added button to show supply requisition history. 2024-02-17 19:13:09 -07:00
Hargata Softworks
bedf7bad34 Merge pull request #306 from hargata/Hargata/report.usability
Improved Usability
2024-02-17 17:13:41 -07:00
DESKTOP-GENO133\IvanPlex
802c7923d5 added supply usage history 2024-02-17 17:07:31 -07:00
DESKTOP-GENO133\IvanPlex
95c8cd19f8 Updated error message 2024-02-17 10:53:29 -07:00
DESKTOP-GENO133\IvanPlex
e700a5f1c2 functionality to duplicate collaborators across vehicles. 2024-02-17 10:51:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7fd7f7e29d added select all option to reports page. 2024-02-16 15:50:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
902ddd0269 improve usability on reports dropdown 2024-02-16 15:29:06 -07:00
Hargata Softworks
0a6424f8c0 Merge pull request #305 from hargata/Hargata/user.provisioning
revert
2024-02-16 14:07:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3a36c624ba revert 2024-02-16 14:07:22 -07:00
Hargata Softworks
1329beb808 Merge pull request #304 from hargata/Hargata/user.provisioning
attempt to fix docker build.
2024-02-16 14:05:40 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0e8ef0180f attempt to fix docker build. 2024-02-16 14:05:01 -07:00
Hargata Softworks
660f089035 Merge pull request #303 from hargata/Hargata/user.provisioning
Hargata/user.provisioning
2024-02-16 13:57:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
04f90ae6a8 Added Distance Traveled Chart 2024-02-16 13:55:22 -07:00
DESKTOP-T0O5CDB\DESK-555BD
30b4a73fef made datepicker honor locale's week start day 2024-02-16 11:58:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b7158f3bf0 automatically log open id user in after they've registered. 2024-02-16 11:06:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f46bbe9963 Allow OpenID Users to sign up with a token. 2024-02-16 10:48:20 -07:00
Hargata Softworks
9f73068a9e Merge pull request #301 from hargata/Hargata/oidc.auth
Updated app version.
2024-02-15 20:12:23 -07:00
DESKTOP-GENO133\IvanPlex
779b18802b Updated app version. 2024-02-15 20:11:39 -07:00
Hargata Softworks
dce9acf47f Merge pull request #300 from hargata/Hargata/oidc.auth
Hargata/OIDC.auth
2024-02-15 20:08:54 -07:00
DESKTOP-GENO133\IvanPlex
c9a925f548 Updated section name 2024-02-15 19:10:38 -07:00
DESKTOP-GENO133\IvanPlex
4eeec7887a Updated wording 2024-02-15 19:09:50 -07:00
Hargata Softworks
156c781bd7 Merge pull request #297 from hargata/Hargata/bugfix
Fixed ExtraFields upsert query.
2024-02-15 19:07:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
296cf92022 added error handling for users not registered with LubeLogger 2024-02-15 19:02:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d5f0e57c3b Added OpenID login. 2024-02-15 18:57:22 -07:00
DESKTOP-GENO133\IvanPlex
86ba6200fc Fixed ExtraFields upsert query. 2024-02-15 09:26:01 -07:00
Hargata Softworks
ac4ea07319 Merge pull request #291 from hargata/Hargata/further.improvements
styling changes.
2024-02-14 12:22:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6d380de603 styling changes. 2024-02-14 12:21:39 -07:00
Hargata Softworks
e789bc6925 Merge pull request #290 from hargata/Hargata/further.improvements
server timezone offset.
2024-02-14 12:15:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f7481be91c Updated version. 2024-02-14 12:14:10 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a8151bcf0e server timezone offset. 2024-02-14 12:12:49 -07:00
Hargata Softworks
0b6a6787bb Merge pull request #289 from hargata/Hargata/further.improvements
Further Improvements
2024-02-14 11:02:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
7302982366 removed empty script tag. 2024-02-14 11:01:08 -07:00
DESKTOP-T0O5CDB\DESK-555BD
19b7dfc90a added calendar view modal 2024-02-14 10:56:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
29825a6ad6 Merge branch 'main' into Hargata/further.improvements
# Conflicts:
#	wwwroot/js/garage.js
2024-02-14 09:21:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f6493f0496 Further improvements to calendar page. 2024-02-14 09:18:47 -07:00
Hargata Softworks
c2d61eecc0 Merge pull request #285 from hargata/Hargata/encode.html
Encode HTML Inputs.
2024-02-13 17:46:13 -07:00
DESKTOP-GENO133\IvanPlex
2a4354c52e Encode HTML Inputs. 2024-02-13 17:45:41 -07:00
Hargata Softworks
e7dc4f67f5 Merge pull request #284 from hargata/Hargata/calendar.view
Lock down contoller methods for extra fields.
2024-02-13 16:48:48 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8584d2cf9c Lock down contoller methods for extra fields. 2024-02-13 16:48:24 -07:00
113 changed files with 3857 additions and 604 deletions

6
.env
View File

@@ -6,4 +6,8 @@ MailConfig__UseSSL="false"
MailConfig__Port=587
MailConfig__Username=""
MailConfig__Password=""
LOGGING__LOGLEVEL__DEFAULT=Error
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;"

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Report a bug
title: ''
labels: ''
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 searched through existing issues.
**Description**
<!-- Describe the bug below this line -->
**Platform**
- [ ] Docker Image
- [ ] Windows Standalone Executable
**Browser Console Errors(F12)**
<!-- Attach a screenshot or codeblock containing the browser console error -->
**App/Container Console Error**
<!-- Attach a screenshot or codeblock containing the app/container console error -->
**Screenshots(optional)**
<!-- Attach a screenshot describing the bug -->

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.1" />
<PackageReference Include="Npgsql" Version="8.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup>
</Project>

View File

@@ -341,6 +341,14 @@ namespace CarCareTracker.Controllers
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/odometerrecords/latest")]
public IActionResult LastOdometer(int vehicleId)
{
var result = GetMaxMileage(vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/odometerrecords")]
public IActionResult OdometerRecords(int vehicleId)
{

View File

@@ -3,8 +3,6 @@ using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Net.Mail;
namespace CarCareTracker.Controllers
{

View File

@@ -1,12 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
using System.Drawing;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using CarCareTracker.Helper;
using Microsoft.AspNetCore.Authorization;

View File

@@ -15,6 +15,7 @@ namespace CarCareTracker.Controllers
private readonly ILogger<HomeController> _logger;
private readonly IVehicleDataAccess _dataAccess;
private readonly IUserLogic _userLogic;
private readonly ILoginLogic _loginLogic;
private readonly IFileHelper _fileHelper;
private readonly IConfigHelper _config;
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
@@ -23,6 +24,7 @@ namespace CarCareTracker.Controllers
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
ILoginLogic loginLogic,
IConfigHelper configuration,
IFileHelper fileHelper,
IExtraFieldDataAccess extraFieldDataAccess,
@@ -37,6 +39,7 @@ namespace CarCareTracker.Controllers
_extraFieldDataAccess = extraFieldDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
_loginLogic = loginLogic;
}
private int GetUserID()
{
@@ -63,21 +66,26 @@ namespace CarCareTracker.Controllers
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
List<ReminderRecordViewModel> reminders = new List<ReminderRecordViewModel>();
foreach(Vehicle vehicle in vehiclesStored)
foreach (Vehicle vehicle in vehiclesStored)
{
var vehicleReminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
vehicleReminders.RemoveAll(x => x.Metric == ReminderMetric.Odometer);
//we don't care about mileages so we can basically fake the current vehicle mileage.
if (vehicleReminders.Any())
{
var maxMileage = vehicleReminders.Max(x => x.Mileage) + 1000;
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, maxMileage, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, 0, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
reminders.AddRange(reminderUrgency);
}
}
return PartialView("_Calendar", reminders);
}
public IActionResult ViewCalendarReminder(int reminderId)
{
var reminder = _reminderRecordDataAccess.GetReminderRecordById(reminderId);
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, 0, DateTime.Now).FirstOrDefault();
return PartialView("_ReminderRecordCalendarModal", reminderUrgency);
}
public IActionResult Settings()
{
var userConfig = _config.GetUserConfig(User);
@@ -92,13 +100,28 @@ namespace CarCareTracker.Controllers
[HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig)
{
//retrieve existing userConfig.
var existingConfig = _config.GetUserConfig(User);
//copy over stuff that persists
userConfig.UserColumnPreferences = existingConfig.UserColumnPreferences;
userConfig.ReminderUrgencyConfig = existingConfig.ReminderUrgencyConfig;
var result = _config.SaveUserConfig(User, userConfig);
return Json(result);
}
[HttpPost]
public IActionResult SaveReminderUrgencyThreshold(ReminderUrgencyConfig reminderUrgencyConfig)
{
//retrieve existing userConfig.
var existingConfig = _config.GetUserConfig(User);
existingConfig.ReminderUrgencyConfig = reminderUrgencyConfig;
var result = _config.SaveUserConfig(User, existingConfig);
return Json(result);
}
public IActionResult Privacy()
{
return View();
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult GetExtraFieldsModal(int importMode = 0)
{
var recordExtraFields = _extraFieldDataAccess.GetExtraFieldsById(importMode);
@@ -108,6 +131,7 @@ namespace CarCareTracker.Controllers
}
return PartialView("_ExtraFields", recordExtraFields);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult UpdateExtraFields(RecordExtraField record)
{
try
@@ -121,6 +145,51 @@ namespace CarCareTracker.Controllers
var recordExtraFields = _extraFieldDataAccess.GetExtraFieldsById(record.Id);
return PartialView("_ExtraFields", recordExtraFields);
}
[HttpPost]
public IActionResult GenerateTokenForUser()
{
try
{
//get current user email address.
var emailAddress = User.FindFirstValue(ClaimTypes.Email);
if (!string.IsNullOrWhiteSpace(emailAddress))
{
var result = _loginLogic.GenerateTokenForEmailAddress(emailAddress, false);
return Json(result);
}
return Json(false);
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(false);
}
}
[HttpPost]
public IActionResult UpdateUserAccount(LoginModel userAccount)
{
try
{
var userId = GetUserID();
if (userId > 0)
{
var result = _loginLogic.UpdateUserDetails(userId, userAccount);
return Json(result);
}
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage});
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
}
[HttpGet]
public IActionResult GetUserAccountInformationModal()
{
var emailAddress = User.FindFirstValue(ClaimTypes.Email);
var userName = User.Identity.Name;
return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName });
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{

View File

@@ -4,9 +4,7 @@ using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
namespace CarCareTracker.Controllers
@@ -15,16 +13,19 @@ namespace CarCareTracker.Controllers
{
private IDataProtector _dataProtector;
private ILoginLogic _loginLogic;
private IConfigHelper _config;
private readonly ILogger<LoginController> _logger;
public LoginController(
ILogger<LoginController> logger,
IDataProtectionProvider securityProvider,
ILoginLogic loginLogic
)
ILoginLogic loginLogic,
IConfigHelper config
)
{
_dataProtector = securityProvider.CreateProtector("login");
_logger = logger;
_loginLogic = loginLogic;
_config = config;
}
public IActionResult Index(string redirectURL = "")
{
@@ -42,6 +43,100 @@ namespace CarCareTracker.Controllers
{
return View();
}
public IActionResult GetRemoteLoginLink()
{
var remoteAuthConfig = _config.GetOpenIDConfig();
var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
remoteAuthConfig.State = generatedState;
if (remoteAuthConfig.ValidateState)
{
Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
}
var remoteAuthURL = remoteAuthConfig.RemoteAuthURL;
return Json(remoteAuthURL);
}
public async Task<IActionResult> RemoteAuth(string code, string state = "")
{
try
{
if (!string.IsNullOrWhiteSpace(code))
{
//received code from OIDC provider
//create http client to retrieve user token from OIDC
var httpClient = new HttpClient();
var openIdConfig = _config.GetOpenIDConfig();
//check if validate state is enabled.
if (openIdConfig.ValidateState)
{
var storedStateValue = Request.Cookies["OIDC_STATE"];
if (!string.IsNullOrWhiteSpace(storedStateValue))
{
Response.Cookies.Delete("OIDC_STATE");
}
if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state)
{
_logger.LogInformation("Failed OIDC State Validation - Try disabling state validation if you are confident this is not a malicious attempt.");
return new RedirectResult("/Login");
}
}
var httpParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("client_id", openIdConfig.ClientId),
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
};
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
{
Content = new FormUrlEncodedContent(httpParams)
};
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt))
{
//validate JWT token
var tokenParser = new JwtSecurityTokenHandler();
var parsedToken = tokenParser.ReadJwtToken(userJwt);
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
if (!string.IsNullOrWhiteSpace(userEmailAddress))
{
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
if (userData.Id != default)
{
AuthCookie authCookie = new AuthCookie
{
UserData = userData,
ExpiresOn = DateTime.Now.AddDays(1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
return new RedirectResult("/Home");
} else
{
_logger.LogInformation($"User {userEmailAddress} tried to login via OpenID but is not a registered user in LubeLogger.");
return View("OpenIDRegistration", model: userEmailAddress);
}
} else
{
_logger.LogInformation("OpenID Provider did not provide a valid email address for the user");
}
} else
{
_logger.LogInformation("OpenID Provider did not provide a valid id_token");
}
} else
{
_logger.LogInformation("OpenID Provider did not provide a code.");
}
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return new RedirectResult("/Login");
}
return new RedirectResult("/Login");
}
[HttpPost]
public IActionResult Login(LoginModel credentials)
{
@@ -81,6 +176,27 @@ namespace CarCareTracker.Controllers
return Json(result);
}
[HttpPost]
public IActionResult RegisterOpenIdUser(LoginModel credentials)
{
var result = _loginLogic.RegisterOpenIdUser(credentials);
if (result.Success)
{
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = credentials.EmailAddress });
if (userData.Id != default)
{
AuthCookie authCookie = new AuthCookie
{
UserData = userData,
ExpiresOn = DateTime.Now.AddDays(1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
}
}
return Json(result);
}
[HttpPost]
public IActionResult RequestResetPassword(LoginModel credentials)
{
var result = _loginLogic.RequestResetPassword(credentials);

View File

@@ -1,13 +1,10 @@
using CarCareTracker.External.Implementations;
using CarCareTracker.Helper;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Npgsql;
using System.Data.Common;
using System.IO.Compression;
using System.Xml.Linq;
namespace CarCareTracker.Controllers
{

View File

@@ -97,13 +97,14 @@ namespace CarCareTracker.Controllers
[HttpGet]
public IActionResult AddVehiclePartialView()
{
return PartialView("_VehicleModal", new Vehicle());
return PartialView("_VehicleModal", new Vehicle() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.VehicleRecord).ExtraFields });
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetEditVehiclePartialViewById(int vehicleId)
{
var data = _dataAccess.GetVehicleById(vehicleId);
data.ExtraFields = StaticHelper.AddExtraFields(data.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.VehicleRecord).ExtraFields);
return PartialView("_VehicleModal", data);
}
[HttpPost]
@@ -154,6 +155,37 @@ namespace CarCareTracker.Controllers
_dataAccess.DeleteVehicle(vehicleId);
return Json(result);
}
[HttpPost]
public IActionResult DuplicateVehicleCollaborators(int sourceVehicleId, int destVehicleId)
{
try
{
//retrieve collaborators for both source and destination vehicle id.
if (_userLogic.UserCanEditVehicle(GetUserID(), sourceVehicleId) && _userLogic.UserCanEditVehicle(GetUserID(), destVehicleId))
{
var sourceCollaborators = _userLogic.GetCollaboratorsForVehicle(sourceVehicleId).Select(x => x.UserVehicle.UserId).ToList();
var destCollaborators = _userLogic.GetCollaboratorsForVehicle(destVehicleId).Select(x => x.UserVehicle.UserId).ToList();
sourceCollaborators.RemoveAll(x => destCollaborators.Contains(x));
if (sourceCollaborators.Any())
{
foreach (int collaboratorId in sourceCollaborators)
{
_userLogic.AddUserAccessToVehicle(collaboratorId, destVehicleId);
}
}
else
{
return Json(new OperationResponse { Success = false, Message = "Both vehicles already have identical collaborators" });
}
}
return Json(new OperationResponse { Success = true, Message = "Collaborators Copied" });
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
}
#region "Bulk Imports and Exports"
[HttpGet]
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
@@ -426,6 +458,16 @@ namespace CarCareTracker.Controllers
if (convertedRecord.Gallons > 0)
{
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(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)
@@ -441,6 +483,16 @@ namespace CarCareTracker.Controllers
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(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)
{
@@ -486,6 +538,16 @@ namespace CarCareTracker.Controllers
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(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)
{
@@ -500,6 +562,16 @@ namespace CarCareTracker.Controllers
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(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)
{
@@ -672,11 +744,16 @@ namespace CarCareTracker.Controllers
}
//move files from temp.
serviceRecord.Files = serviceRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
if (result && serviceRecord.Supplies.Any())
if (serviceRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(serviceRecord.Supplies);
serviceRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(serviceRecord.Supplies, DateTime.Parse(serviceRecord.Date), serviceRecord.Description);
}
//push back any reminders
if (serviceRecord.ReminderRecordId != default)
{
PushbackRecurringReminderRecordWithChecks(serviceRecord.ReminderRecordId);
}
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
return Json(result);
}
[HttpGet]
@@ -700,6 +777,7 @@ namespace CarCareTracker.Controllers
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);
@@ -743,11 +821,16 @@ namespace CarCareTracker.Controllers
}
//move files from temp.
collisionRecord.Files = collisionRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
if (result && collisionRecord.Supplies.Any())
if (collisionRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(collisionRecord.Supplies);
collisionRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(collisionRecord.Supplies, DateTime.Parse(collisionRecord.Date), collisionRecord.Description);
}
//push back any reminders
if (collisionRecord.ReminderRecordId != default)
{
PushbackRecurringReminderRecordWithChecks(collisionRecord.ReminderRecordId);
}
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
return Json(result);
}
[HttpGet]
@@ -771,6 +854,7 @@ namespace CarCareTracker.Controllers
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);
@@ -805,17 +889,19 @@ namespace CarCareTracker.Controllers
var recurringFees = result.Where(x => x.IsRecurring);
if (recurringFees.Any())
{
foreach(TaxRecord recurringFee in recurringFees)
foreach (TaxRecord recurringFee in recurringFees)
{
var newDate = new DateTime();
if (recurringFee.RecurringInterval != ReminderMonthInterval.Other)
{
newDate = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
} else
}
else
{
newDate = recurringFee.Date.AddMonths(recurringFee.CustomMonthInterval);
}
if (DateTime.Now > newDate){
if (DateTime.Now > newDate)
{
recurringFee.IsRecurring = false;
var newRecurringFee = new TaxRecord()
{
@@ -842,6 +928,11 @@ namespace CarCareTracker.Controllers
{
//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 != default)
{
PushbackRecurringReminderRecordWithChecks(taxRecord.ReminderRecordId);
}
var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord());
return Json(result);
}
@@ -890,6 +981,7 @@ namespace CarCareTracker.Controllers
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
var viewModel = new ReportViewModel();
//get totalCostMakeUp
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
@@ -907,10 +999,12 @@ namespace CarCareTracker.Controllers
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)
Cost = x.Sum(y => y.Cost),
DistanceTraveled = x.Any(y => y.MinMileage != default) ? x.Max(y => y.MaxMileage) - x.Where(y => y.MinMileage != default).Min(y => y.MinMileage) : 0
}).ToList();
//get reminders
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now);
@@ -962,7 +1056,7 @@ namespace CarCareTracker.Controllers
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y=>y.Cost)
Cost = x.Sum(y => y.Cost)
}).ToList();
viewModel.FuelMileageForVehicleByMonth = monthlyMileageData;
return PartialView("_Report", viewModel);
@@ -1033,8 +1127,9 @@ namespace CarCareTracker.Controllers
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());
if (exportTabs.Contains(ImportMode.ServiceRecord))
{
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
@@ -1101,7 +1196,8 @@ namespace CarCareTracker.Controllers
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
return Json(new OperationResponse { Success = true, Message = result });
} else
}
else
{
return Json(new OperationResponse { Success = false, Message = "No Attachments Found" });
}
@@ -1111,7 +1207,26 @@ namespace CarCareTracker.Controllers
{
var vehicleHistory = new VehicleHistoryViewModel();
vehicleHistory.VehicleData = _dataAccess.GetVehicleById(vehicleId);
vehicleHistory.Odometer = GetMaxMileage(vehicleId).ToString("N0");
var maxMileage = GetMaxMileage(vehicleId);
vehicleHistory.Odometer = maxMileage.ToString("N0");
var minMileage = GetMinMileage(vehicleId);
var distanceTraveled = maxMileage - minMileage;
if (!string.IsNullOrWhiteSpace(vehicleHistory.VehicleData.PurchaseDate))
{
var endDate = vehicleHistory.VehicleData.SoldDate;
if (string.IsNullOrWhiteSpace(endDate))
{
endDate = DateTime.Now.ToShortDateString();
}
try
{
vehicleHistory.DaysOwned = (DateTime.Parse(endDate) - DateTime.Parse(vehicleHistory.VehicleData.PurchaseDate)).Days.ToString("N0");
} catch (Exception ex)
{
_logger.LogError(ex.Message);
vehicleHistory.DaysOwned = string.Empty;
}
}
List<GenericReportModel> reportData = new List<GenericReportModel>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
@@ -1121,8 +1236,15 @@ namespace CarCareTracker.Controllers
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())
@@ -1239,10 +1361,16 @@ namespace CarCareTracker.Controllers
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)
Cost = x.Sum(y => y.Cost),
DistanceTraveled = x.Any(y => y.MinMileage != default) ? x.Max(y => y.MaxMileage) - x.Where(y => y.MinMileage != default).Min(y => y.MinMileage) : 0
}).ToList();
return PartialView("_GasCostByMonthReport", groupedRecord);
}
@@ -1279,6 +1407,37 @@ namespace CarCareTracker.Controllers
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
[TypeFilter(typeof(CollaboratorFilter))]
private 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;
}
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
{
var currentMileage = GetMaxMileage(vehicleId);
@@ -1327,15 +1486,48 @@ namespace CarCareTracker.Controllers
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 existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder);
//save to db.
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
var result = PushbackRecurringReminderRecordWithChecks(reminderRecordId);
return Json(result);
}
private bool PushbackRecurringReminderRecordWithChecks(int reminderRecordId)
{
try
{
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
if (existingReminder is not null && existingReminder.Id != default && existingReminder.IsRecurring)
{
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder);
//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)
{
@@ -1415,11 +1607,16 @@ namespace CarCareTracker.Controllers
}
//move files from temp.
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
if (result && upgradeRecord.Supplies.Any())
if (upgradeRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies);
upgradeRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Description);
}
//push back any reminders
if (upgradeRecord.ReminderRecordId != default)
{
PushbackRecurringReminderRecordWithChecks(upgradeRecord.ReminderRecordId);
}
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
return Json(result);
}
[HttpGet]
@@ -1443,6 +1640,7 @@ namespace CarCareTracker.Controllers
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);
@@ -1468,7 +1666,7 @@ namespace CarCareTracker.Controllers
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.Where(x=>x.Pinned).ToList();
result = result.Where(x => x.Pinned).ToList();
return Json(result);
}
[HttpPost]
@@ -1494,6 +1692,24 @@ namespace CarCareTracker.Controllers
var result = _noteDataAccess.DeleteNoteById(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);
}
#endregion
#region "Supply Records"
private List<string> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
@@ -1515,20 +1731,41 @@ namespace CarCareTracker.Controllers
}
return result;
}
private void RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage)
private List<SupplyUsageHistory> RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage, DateTime dateRequisitioned, string usageDescription)
{
foreach(SupplyUsage supply in supplyUsage)
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;
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]
@@ -1598,6 +1835,7 @@ namespace CarCareTracker.Controllers
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);
@@ -1628,21 +1866,21 @@ namespace CarCareTracker.Controllers
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();
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
if (result && planRecord.Supplies.Any())
if (planRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(planRecord.Supplies);
planRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description);
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
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();
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
if (existingRecord)
{
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle"});
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);
@@ -1678,15 +1916,24 @@ namespace CarCareTracker.Controllers
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;
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
if (result && existingRecord.Supplies.Any())
if (existingRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(existingRecord.Supplies);
existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description);
}
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
}
[HttpGet]
@@ -1695,6 +1942,16 @@ namespace CarCareTracker.Controllers
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);
@@ -1726,6 +1983,7 @@ namespace CarCareTracker.Controllers
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
@@ -1741,6 +1999,7 @@ namespace CarCareTracker.Controllers
Cost = existingRecord.Cost,
Notes = existingRecord.Notes,
Files = existingRecord.Files,
RequisitionHistory = existingRecord.RequisitionHistory,
ExtraFields = existingRecord.ExtraFields
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
@@ -1756,10 +2015,16 @@ namespace CarCareTracker.Controllers
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);
}
}
return Json(result);
}
@@ -1781,6 +2046,8 @@ namespace CarCareTracker.Controllers
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);
@@ -1896,6 +2163,314 @@ namespace CarCareTracker.Controllers
}
return Json(result);
}
public IActionResult MoveRecords(List<int> recordIds, ImportMode source, ImportMode destination)
{
var genericRecord = new GenericRecord();
bool result = false;
foreach (int recordId in recordIds)
{
//get
switch (source)
{
case ImportMode.ServiceRecord:
genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
genericRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
break;
}
//save
switch (destination)
{
case ImportMode.ServiceRecord:
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord));
break;
case ImportMode.RepairRecord:
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(StaticHelper.GenericToRepairRecord(genericRecord));
break;
case ImportMode.UpgradeRecord:
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord));
break;
}
//delete
if (result)
{
switch (source)
{
case ImportMode.ServiceRecord:
_serviceRecordDataAccess.DeleteServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
_collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
_upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
break;
}
}
}
return Json(result);
}
public IActionResult DeleteRecords(List<int> recordIds, ImportMode importMode)
{
bool result = false;
foreach (int recordId in recordIds)
{
switch (importMode)
{
case ImportMode.ServiceRecord:
result = _serviceRecordDataAccess.DeleteServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
result = _collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
break;
case ImportMode.GasRecord:
result = _gasRecordDataAccess.DeleteGasRecordById(recordId);
break;
case ImportMode.TaxRecord:
result = _taxRecordDataAccess.DeleteTaxRecordById(recordId);
break;
case ImportMode.SupplyRecord:
result = _supplyRecordDataAccess.DeleteSupplyRecordById(recordId);
break;
case ImportMode.NoteRecord:
result = _noteDataAccess.DeleteNoteById(recordId);
break;
case ImportMode.OdometerRecord:
result = _odometerRecordDataAccess.DeleteOdometerRecordById(recordId);
break;
case ImportMode.ReminderRecord:
result = _reminderRecordDataAccess.DeleteReminderRecordById(recordId);
break;
}
}
return Json(result);
}
public IActionResult DuplicateRecords(List<int> recordIds, ImportMode importMode)
{
bool result = false;
foreach (int recordId in recordIds)
{
switch (importMode)
{
case ImportMode.ServiceRecord:
{
var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
existingRecord.Id = default;
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord);
}
break;
case ImportMode.RepairRecord:
{
var existingRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
existingRecord.Id = default;
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord);
}
break;
case ImportMode.UpgradeRecord:
{
var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
existingRecord.Id = default;
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord);
}
break;
case ImportMode.GasRecord:
{
var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId);
existingRecord.Id = default;
result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord);
}
break;
case ImportMode.TaxRecord:
{
var existingRecord = _taxRecordDataAccess.GetTaxRecordById(recordId);
existingRecord.Id = default;
result = _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord);
}
break;
case ImportMode.SupplyRecord:
{
var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(recordId);
existingRecord.Id = default;
result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(existingRecord);
}
break;
case ImportMode.NoteRecord:
{
var existingRecord = _noteDataAccess.GetNoteById(recordId);
existingRecord.Id = default;
result = _noteDataAccess.SaveNoteToVehicle(existingRecord);
}
break;
case ImportMode.OdometerRecord:
{
var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId);
existingRecord.Id = default;
result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord);
}
break;
case ImportMode.ReminderRecord:
{
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(recordId);
existingRecord.Id = default;
result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord);
}
break;
}
}
return Json(result);
}
[HttpPost]
public IActionResult GetGenericRecordModal(List<int> recordIds, ImportMode dataType)
{
return PartialView("_GenericRecordModal", new GenericRecordEditModel() { DataType = dataType, RecordIds = recordIds });
}
[HttpPost]
public IActionResult EditMultipleRecords(GenericRecordEditModel genericRecordEditModel)
{
var dateIsEdited = genericRecordEditModel.EditRecord.Date != default;
var descriptionIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Description);
var mileageIsEdited = genericRecordEditModel.EditRecord.Mileage != default;
var costIsEdited = genericRecordEditModel.EditRecord.Cost != default;
var noteIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Notes);
var tagsIsEdited = genericRecordEditModel.EditRecord.Tags.Any();
//handle clear overrides
if (tagsIsEdited && genericRecordEditModel.EditRecord.Tags.Contains("---"))
{
genericRecordEditModel.EditRecord.Tags = new List<string>();
}
if (noteIsEdited && genericRecordEditModel.EditRecord.Notes == "---")
{
genericRecordEditModel.EditRecord.Notes = "";
}
bool result = false;
foreach (int recordId in genericRecordEditModel.RecordIds)
{
switch (genericRecordEditModel.DataType)
{
case ImportMode.ServiceRecord:
{
var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
if (dateIsEdited)
{
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
}
if (descriptionIsEdited)
{
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
}
if (mileageIsEdited)
{
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
}
if (costIsEdited)
{
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
}
if (noteIsEdited)
{
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
}
if (tagsIsEdited)
{
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
}
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord);
}
break;
case ImportMode.RepairRecord:
{
var existingRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
if (dateIsEdited)
{
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
}
if (descriptionIsEdited)
{
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
}
if (mileageIsEdited)
{
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
}
if (costIsEdited)
{
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
}
if (noteIsEdited)
{
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
}
if (tagsIsEdited)
{
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
}
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord);
}
break;
case ImportMode.UpgradeRecord:
{
var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
if (dateIsEdited)
{
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
}
if (descriptionIsEdited)
{
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
}
if (mileageIsEdited)
{
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
}
if (costIsEdited)
{
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
}
if (noteIsEdited)
{
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
}
if (tagsIsEdited)
{
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
}
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord);
}
break;
}
}
return Json(result);
}
[HttpPost]
public IActionResult SaveUserColumnPreferences(UserColumnPreference columnPreference)
{
try
{
var userConfig = _config.GetUserConfig(User);
var existingUserColumnPreference = userConfig.UserColumnPreferences.Where(x => x.Tab == columnPreference.Tab);
if (existingUserColumnPreference.Any())
{
var existingPreference = existingUserColumnPreference.Single();
existingPreference.VisibleColumns = columnPreference.VisibleColumns;
} else
{
userConfig.UserColumnPreferences.Add(columnPreference);
}
var result = _config.SaveUserConfig(User, userConfig);
return Json(result);
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(false);
}
}
#endregion
}
}

View File

@@ -12,6 +12,7 @@
SupplyRecord = 7,
Dashboard = 8,
PlanRecord = 9,
OdometerRecord = 10
OdometerRecord = 10,
VehicleRecord = 11
}
}

View File

@@ -58,11 +58,7 @@ namespace CarCareTracker.External.Implementations
try
{
var existingRecord = GetExtraFieldsById(record.Id);
string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb))";
if (existingRecord.ExtraFields != null)
{
cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id";
}
string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb)) ON CONFLICT(id) DO UPDATE SET data = CAST(@data AS jsonb)";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", record.Id);

View File

@@ -7,12 +7,17 @@ namespace CarCareTracker.Helper
{
public interface IConfigHelper
{
OpenIDConfig GetOpenIDConfig();
ReminderUrgencyConfig GetReminderUrgencyConfig();
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
bool AuthenticateRootUser(string username, string password);
string GetMOTD();
string GetLogoUrl();
string GetServerLanguage();
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
public bool DeleteUserConfig(int userId);
}
public class ConfigHelper : IConfigHelper
@@ -28,6 +33,25 @@ namespace CarCareTracker.Helper
_userConfig = userConfig;
_cache = memoryCache;
}
public string GetMOTD()
{
var motd = _config["LUBELOGGER_MOTD"];
if (string.IsNullOrWhiteSpace(motd))
{
motd = "";
}
return motd;
}
public OpenIDConfig GetOpenIDConfig()
{
OpenIDConfig openIdConfig = _config.GetSection("OpenIDConfig").Get<OpenIDConfig>() ?? new OpenIDConfig();
return openIdConfig;
}
public ReminderUrgencyConfig GetReminderUrgencyConfig()
{
ReminderUrgencyConfig reminderUrgencyConfig = _config.GetSection("ReminderUrgencyConfig").Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig();
return reminderUrgencyConfig;
}
public string GetLogoUrl()
{
var logoUrl = _config["LUBELOGGER_LOGO_URL"];
@@ -37,6 +61,24 @@ namespace CarCareTracker.Helper
}
return logoUrl;
}
public string GetAllowedFileUploadExtensions()
{
var allowedFileExtensions = _config["LUBELOGGER_ALLOWED_FILE_EXTENSIONS"];
if (string.IsNullOrWhiteSpace(allowedFileExtensions)){
return StaticHelper.DefaultAllowedFileExtensions;
}
return allowedFileExtensions;
}
public bool AuthenticateRootUser(string username, string password)
{
var rootUsername = _config[nameof(UserConfig.UserNameHash)] ?? string.Empty;
var rootPassword = _config[nameof(UserConfig.UserPasswordHash)] ?? string.Empty;
if (string.IsNullOrWhiteSpace(rootUsername) || string.IsNullOrWhiteSpace(rootPassword))
{
return false;
}
return username == rootUsername && password == rootPassword;
}
public string GetServerLanguage()
{
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
@@ -75,20 +117,9 @@ namespace CarCareTracker.Helper
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
configData.EnableAuth = existingUserConfig.EnableAuth;
configData.UserNameHash = existingUserConfig.UserNameHash;
configData.UserPasswordHash = existingUserConfig.UserPasswordHash;
}
else
{
configData.EnableAuth = false;
configData.UserNameHash = string.Empty;
configData.UserPasswordHash = string.Empty;
}
configData.EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)] ?? "false");
configData.UserNameHash = _config[nameof(UserConfig.UserNameHash)] ?? string.Empty;
configData.UserPasswordHash = _config[nameof(UserConfig.UserPasswordHash)] ?? string.Empty;
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(configData));
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return true;
@@ -133,8 +164,12 @@ namespace CarCareTracker.Helper
PreferredGasMileageUnit = _config[nameof(UserConfig.PreferredGasMileageUnit)],
PreferredGasUnit = _config[nameof(UserConfig.PreferredGasUnit)],
UserLanguage = _config[nameof(UserConfig.UserLanguage)],
HideSoldVehicles = bool.Parse(_config[nameof(UserConfig.HideSoldVehicles)]),
EnableShopSupplies = bool.Parse(_config[nameof(UserConfig.EnableShopSupplies)]),
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
EnableExtraFieldColumns = bool.Parse(_config[nameof(UserConfig.EnableExtraFieldColumns)]),
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).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)])
};
int userId = 0;

View File

@@ -71,7 +71,8 @@ namespace CarCareTracker.Helper
IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes,
Tags = currentObject.Tags
Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields
};
if (currentObject.MissedFuelUp)
{
@@ -124,7 +125,8 @@ namespace CarCareTracker.Helper
IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes,
Tags = currentObject.Tags
Tags = currentObject.Tags,
ExtraFields = currentObject.ExtraFields
});
}
previousMileage = currentObject.Mileage;

View File

@@ -8,6 +8,7 @@ namespace CarCareTracker.Helper
{
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
}
public class MailHelper : IMailHelper
@@ -64,6 +65,28 @@ namespace CarCareTracker.Helper
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token))
{
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
}
string emailSubject = "Your User Account Update Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please update your account for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))

View File

@@ -9,6 +9,11 @@ namespace CarCareTracker.Helper
}
public class ReminderHelper: IReminderHelper
{
private readonly IConfigHelper _config;
public ReminderHelper(IConfigHelper config)
{
_config = config;
}
public ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder)
{
if (existingReminder.Metric == ReminderMetric.Both)
@@ -56,6 +61,7 @@ namespace CarCareTracker.Helper
public List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare)
{
List<ReminderRecordViewModel> reminderViewModels = new List<ReminderRecordViewModel>();
var reminderUrgencyConfig = _config.GetReminderUrgencyConfig();
foreach (var reminder in reminders)
{
var reminderViewModel = new ReminderRecordViewModel()
@@ -81,24 +87,24 @@ namespace CarCareTracker.Helper
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < dateCompare.AddDays(7))
else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.VeryUrgentDays))
{
//if less than a week from today or less than 50 miles from current mileage then very urgent.
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
//have to specify by which metric this reminder is urgent.
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 50)
else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.VeryUrgentDistance)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < dateCompare.AddDays(30))
else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.UrgentDays))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 100)
else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.UrgentDistance)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
@@ -110,11 +116,11 @@ namespace CarCareTracker.Helper
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
}
else if (reminder.Date < dateCompare.AddDays(7))
else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.VeryUrgentDays))
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Date < dateCompare.AddDays(30))
else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.UrgentDays))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
@@ -126,11 +132,11 @@ namespace CarCareTracker.Helper
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Mileage < currentMileage + 50)
else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.VeryUrgentDistance)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Mileage < currentMileage + 100)
else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.UrgentDistance)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}

View File

@@ -5,6 +5,7 @@ namespace CarCareTracker.Helper
{
public interface IReportHelper
{
IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0);
@@ -13,6 +14,21 @@ namespace CarCareTracker.Helper
}
public class ReportHelper: IReportHelper
{
public IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0)
{
if (year != default)
{
odometerRecords.RemoveAll(x => x.Date.Year != year);
}
return odometerRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = 0,
MaxMileage = x.Max(y => y.Mileage),
MinMileage = x.Min(y => y.Mileage)
});
}
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0)
{
if (year != default)
@@ -23,7 +39,9 @@ namespace CarCareTracker.Helper
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
Cost = x.Sum(y => y.Cost),
MaxMileage = x.Max(y=>y.Mileage),
MinMileage = x.Min(y=>y.Mileage)
});
}
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0)
@@ -36,7 +54,9 @@ namespace CarCareTracker.Helper
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
Cost = x.Sum(y => y.Cost),
MaxMileage = x.Max(y => y.Mileage),
MinMileage = x.Min(y => y.Mileage)
});
}
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0)
@@ -49,7 +69,9 @@ namespace CarCareTracker.Helper
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
Cost = x.Sum(y => y.Cost),
MaxMileage = x.Max(y => y.Mileage),
MinMileage = x.Min(y => y.Mileage)
});
}
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0)
@@ -62,7 +84,9 @@ namespace CarCareTracker.Helper
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
Cost = x.Sum(y => y.Cost),
MaxMileage = x.Max(y => y.Mileage),
MinMileage = x.Min(y => y.Mileage)
});
}
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0)

View File

@@ -1,6 +1,5 @@
using CarCareTracker.Models;
using System.Globalization;
using System.Linq;
namespace CarCareTracker.Helper
{
@@ -9,10 +8,12 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.2.5";
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
{
@@ -130,7 +131,8 @@ namespace CarCareTracker.Helper
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags,
ExtraFields = input.ExtraFields
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
@@ -145,7 +147,8 @@ namespace CarCareTracker.Helper
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags,
ExtraFields = input.ExtraFields
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
@@ -160,7 +163,8 @@ namespace CarCareTracker.Helper
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags,
ExtraFields = input.ExtraFields
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
@@ -180,11 +184,20 @@ namespace CarCareTracker.Helper
{
return templateExtraFields;
}
//append the fields.
var recordFieldNames = recordExtraFields.Select(x => x.Name);
//update isrequired setting
foreach (ExtraField extraField in recordExtraFields)
{
extraField.IsRequired = templateExtraFields.Where(x => x.Name == extraField.Name).First().IsRequired;
}
//append extra fields
foreach(ExtraField extraField in templateExtraFields)
{
if (!recordFieldNames.Contains(extraField.Name))
{
recordExtraFields.Add(extraField);
}
}
return recordExtraFields;
}
@@ -210,5 +223,26 @@ namespace CarCareTracker.Helper
}
return fuelEconomyUnit;
}
public static long GetEpochFromDateTime(DateTime date)
{
return new DateTimeOffset(date).ToUnixTimeMilliseconds();
}
public static void InitMessage(IConfiguration config)
{
Console.WriteLine($"LubeLogger {VersionNumber}");
Console.WriteLine("Website: https://lubelogger.com");
Console.WriteLine("Documentation: https://docs.lubelogger.com");
Console.WriteLine("GitHub: https://github.com/hargata/lubelog");
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
Console.WriteLine($"SMTP Configured for {mailConfig.EmailServer}");
} else
{
Console.WriteLine("SMTP Not Configured");
}
var motd = config["LUBELOGGER_MOTD"] ?? "Not Configured";
Console.WriteLine($"Message Of The Day: {motd}");
}
}
}

View File

@@ -1,5 +1,4 @@
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
namespace CarCareTracker.Helper

View File

@@ -2,8 +2,6 @@
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -16,14 +14,18 @@ namespace CarCareTracker.Logic
OperationResponse GenerateUserToken(string emailAddress, bool autoNotify);
bool DeleteUserToken(int tokenId);
bool DeleteUser(int userId);
OperationResponse RegisterOpenIdUser(LoginModel credentials);
OperationResponse UpdateUserDetails(int userId, LoginModel credentials);
OperationResponse RegisterNewUser(LoginModel credentials);
OperationResponse RequestResetPassword(LoginModel credentials);
OperationResponse ResetPasswordByUser(LoginModel credentials);
OperationResponse ResetUserPassword(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials);
UserData ValidateOpenIDUser(LoginModel credentials);
bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials);
bool DeleteRootUserCredentials();
bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset);
List<UserData> GetAllUsers();
List<Token> GetAllTokens();
@@ -33,15 +35,18 @@ namespace CarCareTracker.Logic
private readonly IUserRecordDataAccess _userData;
private readonly ITokenRecordDataAccess _tokenData;
private readonly IMailHelper _mailHelper;
private readonly IConfigHelper _configHelper;
private IMemoryCache _cache;
public LoginLogic(IUserRecordDataAccess userData,
ITokenRecordDataAccess tokenData,
IMailHelper mailHelper,
IConfigHelper configHelper,
IMemoryCache memoryCache)
{
_userData = userData;
_tokenData = tokenData;
_mailHelper = mailHelper;
_configHelper = configHelper;
_cache = memoryCache;
}
public bool CheckIfUserIsValid(int userId)
@@ -59,6 +64,89 @@ namespace CarCareTracker.Logic
return result.Id != 0;
}
}
public OperationResponse UpdateUserDetails(int userId, LoginModel credentials)
{
//get current user details
var existingUser = _userData.GetUserRecordById(userId);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Invalid user" };
}
//validate user token
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != existingUser.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
}
if (!string.IsNullOrWhiteSpace(credentials.UserName) && existingUser.UserName != credentials.UserName)
{
//check if new username is already taken.
var existingUserWithUserName = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUserWithUserName.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
}
existingUser.UserName = credentials.UserName;
}
if (!string.IsNullOrWhiteSpace(credentials.EmailAddress) && existingUser.EmailAddress != credentials.EmailAddress)
{
//check if email address already exists
var existingUserWithEmailAddress = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmailAddress.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
}
existingUser.EmailAddress = credentials.EmailAddress;
}
if (!string.IsNullOrWhiteSpace(credentials.Password))
{
//update password
existingUser.Password = GetHash(credentials.Password);
}
//delete token
_tokenData.DeleteToken(existingToken.Id);
var result = _userData.SaveUserRecord(existingUser);
return new OperationResponse { Success = result, Message = result ? "User Updated" : StaticHelper.GenericErrorMessage };
}
public OperationResponse RegisterOpenIdUser(LoginModel credentials)
{
//validate their token.
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
}
if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName))
{
return new OperationResponse { Success = false, Message = "Username cannot be blank" };
}
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
}
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmail.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
}
_tokenData.DeleteToken(existingToken.Id);
var newUser = new UserData()
{
UserName = credentials.UserName,
Password = GetHash(NewToken()), //generate a password for OpenID User
EmailAddress = credentials.EmailAddress
};
var result = _userData.SaveUserRecord(newUser);
if (result)
{
return new OperationResponse { Success = true, Message = "You will be logged in briefly." };
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
}
}
//handles user registration
public OperationResponse RegisterNewUser(LoginModel credentials)
{
@@ -112,21 +200,7 @@ namespace CarCareTracker.Logic
if (existingUser.Id != default)
{
//user exists, generate a token and send email.
//check to see if there is an existing token sent to the user.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(existingUser.EmailAddress);
if (existingToken.Id == default)
{
var token = new Token()
{
Body = NewToken(),
EmailAddress = existingUser.EmailAddress
};
var result = _tokenData.CreateNewToken(token);
if (result)
{
result = _mailHelper.NotifyUserForPasswordReset(existingUser.EmailAddress, token.Body).Success;
}
}
GenerateTokenForEmailAddress(existingUser.EmailAddress, true);
}
//for security purposes we want to always return true for this method.
//otherwise someone can spam the reset password method to sniff out users.
@@ -175,7 +249,8 @@ namespace CarCareTracker.Logic
Id = -1,
UserName = credentials.UserName,
IsAdmin = true,
IsRootUser = true
IsRootUser = true,
EmailAddress = string.Empty
};
}
else
@@ -193,6 +268,19 @@ namespace CarCareTracker.Logic
}
}
}
public UserData ValidateOpenIDUser(LoginModel credentials)
{
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (result.Id != default)
{
result.Password = string.Empty;
return result;
}
else
{
return new UserData();
}
}
#region "Admin Functions"
public bool MakeUserAdmin(int userId, bool isAdmin)
{
@@ -280,19 +368,32 @@ namespace CarCareTracker.Logic
#region "Root User"
public bool CreateRootUserCredentials(LoginModel credentials)
{
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
//check if file exists
if (File.Exists(StaticHelper.UserConfigPath))
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName;
existingUserConfig.UserPasswordHash = hashedPassword;
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName;
existingUserConfig.UserPasswordHash = hashedPassword;
}
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
} else
{
var newUserConfig = new UserConfig()
{
EnableAuth = true,
UserNameHash = GetHash(credentials.UserName),
UserPasswordHash = GetHash(credentials.Password)
};
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig));
}
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
_cache.Remove("userConfig_-1");
return true;
}
@@ -314,21 +415,9 @@ namespace CarCareTracker.Logic
}
private bool UserIsRoot(LoginModel credentials)
{
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
//compare against stored hash.
if (hashedUserName == existingUserConfig.UserNameHash &&
hashedPassword == existingUserConfig.UserPasswordHash)
{
return true;
}
}
return false;
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
}
#endregion
private static string GetHash(string value)
@@ -350,5 +439,30 @@ namespace CarCareTracker.Logic
{
return Guid.NewGuid().ToString().Substring(0, 8);
}
public bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset)
{
bool result = false;
//check if there is already a token tied to this email address.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress);
if (existingToken.Id == default)
{
//no token, generate one and send.
var token = new Token()
{
Body = NewToken(),
EmailAddress = emailAddress
};
result = _tokenData.CreateNewToken(token);
if (result)
{
result = isPasswordReset ? _mailHelper.NotifyUserForPasswordReset(emailAddress, token.Body).Success : _mailHelper.NotifyUserForAccountUpdate(emailAddress, token.Body).Success;
}
} else
{
//token exists, send it again.
result = isPasswordReset ? _mailHelper.NotifyUserForPasswordReset(emailAddress, existingToken.Body).Success : _mailHelper.NotifyUserForAccountUpdate(emailAddress, existingToken.Body).Success;
}
return result;
}
}
}

View File

@@ -1,7 +1,6 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace CarCareTracker.Logic
{

View File

@@ -1,9 +1,7 @@
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
@@ -123,6 +121,7 @@ namespace CarCareTracker.Middleware
{
new(ClaimTypes.Name, authCookie.UserData.UserName),
new(ClaimTypes.NameIdentifier, authCookie.UserData.Id.ToString()),
new(ClaimTypes.Email, authCookie.UserData.EmailAddress),
new(ClaimTypes.Role, "CookieAuth")
};
if (authCookie.UserData.IsAdmin)

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -13,6 +14,20 @@
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public CollisionRecord ToCollisionRecord() { return new CollisionRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Mileage = Mileage,
Description = Description,
Notes = Notes,
Files = Files,
Tags = Tags,
ExtraFields = ExtraFields,
RequisitionHistory = RequisitionHistory
};
}
}
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class GenericRecordEditModel
{
public ImportMode DataType { get; set; }
public List<int> RecordIds { get; set; } = new List<int>();
public GenericRecord EditRecord { get; set; } = new GenericRecord();
}
}

View File

@@ -0,0 +1,16 @@
namespace CarCareTracker.Models
{
public class OpenIDConfig
{
public string Name { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string AuthURL { get; set; }
public string TokenURL { get; set; }
public string RedirectURL { get; set; }
public string Scope { get; set; }
public string State { get; set; }
public bool ValidateState { get; set; } = false;
public string RemoteAuthURL { get { return $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; } }
}
}

View File

@@ -0,0 +1,7 @@
namespace CarCareTracker.Models
{
public class OpenIDResult
{
public string id_token { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace CarCareTracker.Models
{
public class PlanCostItem
{
public string CostName { get; set; }
public decimal CostAmount { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
public string Description { get; set; }
@@ -14,5 +15,6 @@
public PlanProgress Progress { get; set; }
public decimal Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public string DateCreated { get; set; } = DateTime.Now.ToShortDateString();
public string DateModified { get; set; } = DateTime.Now.ToShortDateString();
public string Description { get; set; }
@@ -15,9 +16,11 @@
public PlanProgress Progress { get; set; }
public decimal Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public PlanRecord ToPlanRecord() { return new PlanRecord {
Id = Id,
VehicleId = VehicleId,
ReminderRecordId = ReminderRecordId,
DateCreated = DateTime.Parse(DateCreated),
DateModified = DateTime.Parse(DateModified),
Description = Description,
@@ -27,7 +30,12 @@
Cost = Cost,
Priority = Priority,
Progress = Progress,
ExtraFields = ExtraFields
ExtraFields = ExtraFields,
RequisitionHistory = RequisitionHistory
}; }
/// <summary>
/// only used to hide view template button on plan create modal.
/// </summary>
public bool CreatedFromReminder { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public class ReminderUrgencyConfig
{
public int UrgentDays { get; set; } = 30;
public int VeryUrgentDays { get; set; } = 7;
public int UrgentDistance { get; set; } = 100;
public int VeryUrgentDistance { get; set; } = 50;
}
}

View File

@@ -5,5 +5,8 @@
public int MonthId { get; set; }
public string MonthName { get; set; }
public decimal Cost { get; set; }
public int MaxMileage { get; set; }
public int MinMileage { get; set; }
public int DistanceTraveled { get; set; }
}
}

View File

@@ -8,5 +8,10 @@
public string MPG { get; set; }
public decimal TotalCost { get; set; }
public decimal TotalGasCost { get; set; }
public string DaysOwned { get; set; }
public string DistanceTraveled { get; set; }
public decimal TotalCostPerMile { get; set; }
public decimal TotalGasCostPerMile { get; set; }
public string DistanceUnit { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -13,6 +14,20 @@
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public ServiceRecord ToServiceRecord() { return new ServiceRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Mileage = Mileage,
Description = Description,
Notes = Notes,
Files = Files,
Tags = Tags,
ExtraFields = ExtraFields,
RequisitionHistory = RequisitionHistory
};
}
}
}

View File

@@ -12,5 +12,6 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
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>();
}
}

View File

@@ -35,5 +35,6 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
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>();
}
}

View File

@@ -14,6 +14,7 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
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 SupplyRecord ToSupplyRecord() { return new SupplyRecord {
Id = Id,
VehicleId = VehicleId,
@@ -26,7 +27,8 @@
Notes = Notes,
Files = Files,
Tags = Tags,
ExtraFields = ExtraFields
ExtraFields = ExtraFields,
RequisitionHistory = RequisitionHistory
}; }
}
}

View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public class SupplyUsageHistory {
public DateTime Date { get; set; }
public string PartNumber { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
public decimal Cost { get; set; }
}
}

View File

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

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int ReminderRecordId { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
@@ -13,6 +14,20 @@
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Mileage = Mileage,
Description = Description,
Notes = Notes,
Files = Files,
Tags = Tags,
ExtraFields = ExtraFields,
RequisitionHistory = RequisitionHistory
};
}
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class UserColumnPreference
{
public ImportMode Tab { get; set; }
public List<string> VisibleColumns { get; set; } = new List<string>();
}
}

View File

@@ -14,8 +14,12 @@
public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; }
public bool EnableShopSupplies { get; set; }
public bool EnableExtraFieldColumns { get; set; }
public bool HideSoldVehicles { get; set; }
public string PreferredGasUnit { get; set; } = string.Empty;
public string PreferredGasMileageUnit { get; set; } = string.Empty;
public List<UserColumnPreference> UserColumnPreferences { get; set; } = new List<UserColumnPreference>();
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
public string UserLanguage { get; set; } = "en_US";

View File

@@ -8,8 +8,11 @@
public string Make { get; set; }
public string Model { get; set; }
public string LicensePlate { get; set; }
public string PurchaseDate { get; set; }
public string SoldDate { get; set; }
public bool IsElectric { get; set; } = false;
public bool UseHours { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
}
}

View File

@@ -10,6 +10,9 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
//Print Messages
StaticHelper.InitMessage(builder.Configuration);
// Add services to the container.
builder.Services.AddControllersWithViews();

View File

@@ -1,39 +1,28 @@
![image](https://github.com/hargata/lubelog/assets/155338622/545debcd-d80a-44da-b892-4c652ab0384a)
A self-hosted, open-source vehicle service records and maintainence tracker.
Self-Hosted, Open-Source, Web-Based Vehicle Maintenance and Fuel Mileage Tracker
Visit our website: https://lubelogger.com
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
Website: https://lubelogger.com
## Why
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintenance.
## Screenshots
<a href="/docs/screenshots.md">Screenshots</a>
## Showcase
[Promotional Brochure](https://lubelogger.com/brochure.pdf)
[Screenshots](/docs/screenshots.md)
## Demo
Try it out before you download it! The live demo resets every 20 minutes.
[Live Demo](https://demo.lubelogger.com) Login using username "test" and password "1234"
## Dependencies
- Bootstrap
- LiteDB
- Bootstrap-DatePicker
- SweetAlert2
- CsvHelper
- Chart.js
- Drawdown
## Download
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
## Docker Setup (GHCR)
1. Install Docker
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats. You can also setup SMTP Config here.
4. If using traefik, use docker-compose.traefik.yml
5. Run `docker-compose up`
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
## Docker Setup (Manual Build)
### 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.
@@ -42,38 +31,27 @@ Try it out before you download it! The live demo resets every 20 minutes.
6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up`
## Additional Docker Instructions
### Need Help?
[Documentation](https://docs.lubelogger.com/)
### manual
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
- build
[Search Existing Issues](https://github.com/hargata/lubelog/issues)
```
docker build -t hargata/lubelog:latest .
```
## Dependencies
- Bootstrap
- LiteDB
- Npgsql
- Bootstrap-DatePicker
- SweetAlert2
- CsvHelper
- Chart.js
- Drawdown
- run
## License
LubeLogger utilizes a dual-licensing model, see [License](/LICENSE) for more information
```
docker run -d hargata/lubelog:latest
```
## Support
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
add `-v` for persistent volumes as needed. Have a look at the docker-compose.yml for examples.
## docker-compose
- build image
```
docker compose build
```
- run
```
docker compose up
# or variant with traefik labels:
docker compose -f docker-compose.traefik.yml up
```
Note: Commercial users are required to maintain an active Patreon subscripton to be compliant with our licensing model.

View File

@@ -285,6 +285,20 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<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

View File

@@ -24,11 +24,11 @@
@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"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</button>
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</span></button>
@@ -41,6 +41,12 @@
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</span></a>
</li>
}
@if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
<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>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="performLogOut()"><span class="display-3 ms-2"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage,"Logout")</span></button>
</li>
@@ -84,6 +90,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)))
{
<li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
}
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage,"Logout")</button>
</li>
@@ -118,6 +130,12 @@
</div>
</div>
</div>
<div class="modal fade" id="accountInformationModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content" id="accountInformationModalContent">
</div>
</div>
</div>
<script>
loadGarage();
bindWindowResize();

View File

@@ -0,0 +1,35 @@
@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="addVehicleModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="form-inline">
<div class="form-group">
<label for="inputUsername">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Account Username")" value="@Model.UserName">
<label for="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="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Token")" value="">
<div class="row">
<div class="col-12">
<a onclick="generateTokenForUser()" class="btn btn-link">@translator.Translate(userLanguage, "Send Token")</a>
</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="validateAndSaveUserAccount()" class="btn btn-primary">@translator.Translate(userLanguage, "Update")</button>
</div>

View File

@@ -1,10 +1,11 @@
@model List<ReminderRecordViewModel>
@using CarCareTracker.Helper
@model List<ReminderRecordViewModel>
<script>
var eventDates = [];
var groupedDates = [];
@foreach(ReminderRecordViewModel reminderRecord in Model)
{
@:eventDates.push({date: new Date(Date.parse(decodeHTMLEntities('@reminderRecord.Date.ToShortDateString()'))), description: decodeHTMLEntities('@reminderRecord.Description'), urgency: decodeHTMLEntities('@reminderRecord.Urgency.ToString()')});
@:eventDates.push({ id: @reminderRecord.Id, date: decodeHTMLEntities('@StaticHelper.GetEpochFromDateTime(reminderRecord.Date)'), description: decodeHTMLEntities('@reminderRecord.Description'), urgency: decodeHTMLEntities('@reminderRecord.Urgency.ToString()') });
}
</script>
<div class="row vehicleDetailTabContainer">
@@ -13,6 +14,12 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="reminderRecordCalendarModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="reminderRecordCalendarModalContent">
</div>
</div>
</div>
<script>
initCalendar();
</script>

View File

@@ -27,6 +27,7 @@
<!option @(Model.Id == (int)ImportMode.SupplyRecord ? "selected" : "") value="@((int)ImportMode.SupplyRecord)">@translator.Translate(userLanguage, "Supplies")</!option>
<!option @(Model.Id == (int)ImportMode.PlanRecord ? "selected" : "") value="@((int)ImportMode.PlanRecord)">@translator.Translate(userLanguage, "Planner")</!option>
<!option @(Model.Id == (int)ImportMode.OdometerRecord ? "selected" : "") value="@((int)ImportMode.OdometerRecord)">@translator.Translate(userLanguage, "Odometer")</!option>
<!option @(Model.Id == (int)ImportMode.VehicleRecord ? "selected" : "") value="@((int)ImportMode.VehicleRecord)">@translator.Translate(userLanguage, "Vehicle")</!option>
</select>
</div>
</div>

View File

@@ -1,5 +1,10 @@
@model List<Vehicle>
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<Vehicle>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
}
@if (recordTags.Any())
@@ -23,17 +28,24 @@
<div class="row gy-3 align-items-stretch vehiclesContainer">
@foreach (Vehicle vehicle in Model)
{
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none garage-item" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" 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;" />
<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>
@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="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>
}
<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>
</div>
</div>
</div>
</div>
}
}
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 garage-item-add">
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
@@ -41,4 +53,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model ReminderRecordViewModel
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@(translator.Translate(userLanguage, "View Reminder"))</h5>
<button type="button" class="btn-close" onclick="hideCalendarReminderModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12" id="reminderOptions">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="reminderDescription">@translator.Translate(userLanguage, "Date")</label>
<input type="text" id="reminderDescription" readonly class="form-control" value="@Model.Date.ToShortDateString()">
<label for="reminderDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="reminderDescription" readonly class="form-control" value="@Model.Description">
</div>
<div class="col-md-6 col-12">
<label for="reminderNotes">@translator.Translate(userLanguage,"Notes")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="reminderNotes" readonly class="form-control" rows="5">@Model.Notes</textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" onclick="deleteCalendarReminderRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
<button type="button" class="btn btn-secondary" onclick="hideCalendarReminderModal()">@translator.Translate(userLanguage, "Close")</button>
@if (Model.IsRecurring && (Model.Urgency == ReminderUrgency.VeryUrgent || Model.Urgency == ReminderUrgency.PastDue))
{
<button type="button" class="btn btn-primary" onclick="markDoneCalendarReminderRecord(@Model.Id)">@translator.Translate(userLanguage, "Mark as Done")</button>
}
</div>

View File

@@ -53,6 +53,14 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoOdometerInsert" checked="@Model.UserConfig.EnableAutoOdometerInsert">
<label class="form-check-label" for="enableAutoOdometerInsert">@translator.Translate(userLanguage, "Auto Insert Odometer Records")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Only when Adding Service/Repair/Upgrade/Fuel Record or Completing a Plan")</small></label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableExtraFieldColumns" checked="@Model.UserConfig.EnableExtraFieldColumns">
<label class="form-check-label" for="enableExtraFieldColumns">@translator.Translate(userLanguage, "Show Extra Field Columns")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Enabling this may cause performance issues")</small></label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideSoldVehicles" checked="@Model.UserConfig.HideSoldVehicles">
<label class="form-check-label" for="hideSoldVehicles">@translator.Translate(userLanguage, "Hide Sold Vehicles")</label>
</div>
<div class="form-check form-switch @(User.IsInRole(nameof(UserData.IsRootUser)) ? "" : "d-none")">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies">
<label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label>
@@ -180,10 +188,13 @@
</div>
<div class="row">
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Manage Extra Fields")</span>
<span class="lead">@translator.Translate(userLanguage, "Server-wide Settings")</span>
<div class="row">
<div class="col-12 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Add/Remove Extra Fields")</button>
<div class="col-6 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md">@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>
</div>
</div>
</div>
@@ -201,7 +212,7 @@
<img src="/defaults/lubelogger_logo.png" />
</div>
<div class="d-flex justify-content-center">
<small class="text-body-secondary">Version 1.1.6</small>
<small class="text-body-secondary">@($"{translator.Translate(userLanguage, "Version")} {StaticHelper.VersionNumber}")</small>
</div>
<p class="lead">
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
@@ -246,6 +257,53 @@
</div>
</div>
<script>
function showReminderUrgencyThresholdModal(){
Swal.fire({
title: decodeHTMLEntities('@translator.Translate(userLanguage, "Configure Reminder Urgency Thresholds")'),
html: `
<form>
<div class='d-flex flex-row align-items-center'>
<label class='form-label me-auto'>${decodeHTMLEntities('@translator.Translate(userLanguage, "Urgent(Days)")')}</label>
<input type="text" id="inputUrgentDays" class="form-control" style='width:40%' placeholder="${decodeHTMLEntities('@translator.Translate(userLanguage, "Urgent")')}" value='@Model.UserConfig.ReminderUrgencyConfig.UrgentDays'>
</div>
<div class='d-flex flex-row align-items-center mt-2'>
<label class='form-label me-auto'>${decodeHTMLEntities('@translator.Translate(userLanguage, "Very Urgent(Days)")')}</label>
<input type="text" id="inputVeryUrgentDays" class="form-control" style='width:40%' placeholder="${decodeHTMLEntities('@translator.Translate(userLanguage, "Very Urgent")')}" value='@Model.UserConfig.ReminderUrgencyConfig.VeryUrgentDays'>
</div>
<div class='d-flex flex-row align-items-center mt-2'>
<label class='form-label me-auto'>${decodeHTMLEntities('@translator.Translate(userLanguage, "Urgent(Distance)")')}</label>
<input type="text" id="inputUrgentDistance" class="form-control" style='width:40%' placeholder="${decodeHTMLEntities('@translator.Translate(userLanguage, "Urgent")')}" value='@Model.UserConfig.ReminderUrgencyConfig.UrgentDistance'>
</div>
<div class='d-flex flex-row align-items-center mt-2'>
<label class='form-label me-auto'>${decodeHTMLEntities('@translator.Translate(userLanguage, "Very Urgent(Distance)")')}</label>
<input type="text" id="inputVeryUrgentDistance" class="form-control" style='width:40%' placeholder="${decodeHTMLEntities('@translator.Translate(userLanguage, "Very Urgent")')}" value='@Model.UserConfig.ReminderUrgencyConfig.VeryUrgentDistance'>
</div>
</form>
`,
confirmButtonText: decodeHTMLEntities('@translator.Translate(userLanguage, "Save")'),
focusConfirm: false,
preConfirm: () => {
const urgentDays = $("#inputUrgentDays").val();
const veryUrgentDays = $("#inputVeryUrgentDays").val();
const urgentDistance = $("#inputUrgentDistance").val();
const veryUrgentDistance = $("#inputVeryUrgentDistance").val();
if (!urgentDays || isNaN(urgentDays) || !veryUrgentDays || isNaN(veryUrgentDays) || !urgentDistance || isNaN(urgentDistance) || !veryUrgentDistance || isNaN(veryUrgentDistance)) {
Swal.showValidationMessage(`Invalid parameters`)
}
return { urgentDays, veryUrgentDays, urgentDistance, veryUrgentDistance }
},
}).then(function (result) {
if (result.isConfirmed) {
$.post('/Home/SaveReminderUrgencyThreshold', { urgentDays: result.value.urgentDays, veryUrgentDays: result.value.veryUrgentDays, urgentDistance: result.value.urgentDistance, veryUrgentDistance: result.value.veryUrgentDistance }, function (data) {
if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(genericErrorMessage());
}
})
}
});
}
function showExtraFieldModal() {
$.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) {
$("#extraFieldModalContent").html(data);
@@ -287,6 +345,8 @@
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(),

View File

@@ -0,0 +1,10 @@
@model List<ExtraField>
@if (Model.Any())
{
<ul class='list-group list-group-flush'>
@foreach (ExtraField field in Model)
{
<li><b>@field.Name</b> : @field.Value</li>
}
</ul>
}

View File

@@ -5,6 +5,7 @@
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
var openIdConfigName = config.GetOpenIDConfig().Name;
}
@{
ViewData["Title"] = "LubeLogger - Login";
@@ -31,6 +32,17 @@
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Login")</button>
</div>
@if (!string.IsNullOrWhiteSpace(openIdConfigName))
{
<div class="d-grid">
<button type="button" class="btn btn-secondary mt-2" onclick="remoteLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@($"{translator.Translate(userLanguage, "Login via")} {openIdConfigName}")</button>
</div>
}
<div class="d-flex justify-content-center">
<div class="form-group">
<small class="text-body-secondary">@config.GetMOTD()</small>
</div>
</div>
<div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
</div>
@@ -41,7 +53,7 @@
</div>
</div>
<script>
function getRedirectURL(){
function getRedirectURL() {
return { url: decodeHTMLEntities('@Model') };
}
</script>

View File

@@ -0,0 +1,50 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
}
@model string
@{
ViewData["Title"] = "LubeLogger - Register";
}
@section Scripts {
<script src="~/js/login.js"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<img src="@logoUrl" />
<div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control">
</div>
<div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control" value="@Model">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performOpenIdRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Back to Login")</a>
</div>
</div>
</div>
</div>
<script>
function performOpenIdRegistration() {
var token = $("#inputToken").val();
var userName = $("#inputUserName").val();
var userEmail = decodeHTMLEntities('@Model');
$.post('/Login/RegisterOpenIdUser', { userName: userName, token: token, emailAddress: userEmail }, function (data) {
if (data.success) {
successToast(data.message);
setTimeout(function () { window.location.href = '/Home' }, 500);
} else {
errorToast(data.message);
}
});
}
</script>

View File

@@ -10,6 +10,7 @@
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var firstDayOfWeek = (int)System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
var userLanguage = userConfig.UserLanguage;
shortDatePattern = shortDatePattern.ToLower();
@@ -58,7 +59,8 @@
useMarkDown: "@useMarkDown" == "True",
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
useThreeDecimals: "@useThreeDecimals" == "True",
useMPG: "@useMPG" == "True"
useMPG: "@useMPG" == "True",
firstDayOfWeek: @firstDayOfWeek
}
}
function getShortDatePattern() {

View File

@@ -156,6 +156,12 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="genericRecordEditModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="genericRecordEditModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="inputSuppliesModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="inputSuppliesModalContent"></div>

View File

@@ -26,6 +26,14 @@
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
<label for="collisionRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) repaired(i.e. Alternator)")" value="@Model.Description">
@if (isNew)
{
<div class="row">
<div class="col-12">
<a onclick="showRecurringReminderSelector('collisionRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div>
</div>
}
<label for="collisionRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" inputmode="decimal" id="collisionRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the repair")" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
@@ -56,7 +64,7 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="collisionRecordFiles">@translator.Translate(userLanguage,"Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div>
}
@@ -72,7 +80,7 @@
</div>
}
<label for="collisionRecordFiles">@translator.Translate(userLanguage,"Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
</div>
@@ -83,6 +91,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
}
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteCollisionRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
@@ -105,16 +117,18 @@
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Repair Record")</button>
}
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
var selectedSupplies = [];
var recurringReminderRecordId = 0;
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
}
function getCollisionRecordModelData() {
return { id: @Model.Id}
}

View File

@@ -7,6 +7,12 @@
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var userLanguage = userConfig.UserLanguage;
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.RepairRecord);
}
@model List<CollisionRecord>
<div class="row">
@@ -38,6 +44,49 @@
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('accident-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'RepairRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
@@ -58,22 +107,30 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage,"Odometer")</th>
<th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage,"Description")</th>
<th scope="col" class="col-2" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage,"Cost")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage,"Notes")</th>
<th scope="col" class="col-2 col-xl-1 flex-grow-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4 flex-grow-1" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="cost" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (CollisionRecord collisionRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2">@collisionRecord.Mileage</td>
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
<td class="col-2" data-record-type="cost">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@collisionRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditCollisionRecordModal,@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(collisionRecord.Date)">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1" data-column="odometer">@collisionRecord.Mileage</td>
<td class="col-3 col-xl-4 flex-grow-1" data-column="description">@collisionRecord.Description</td>
<td class="col-2 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(collisionRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -87,4 +144,23 @@
<div class="modal-content" id="collisionRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'RepairRecord', 'ServiceRecord')">@translator.Translate(userLanguage, "Service Records")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'RepairRecord', 'UpgradeRecord')">@translator.Translate(userLanguage, "Upgrades")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)">@translator.Translate(userLanguage, "Statistics")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -29,7 +29,8 @@
{
consumptionUnit = "imp gal";
fuelEconomyUnit = useHours ? "h/g" : "mpg";
} else if (useUKMPG)
}
else if (useUKMPG)
{
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
consumptionUnit = "l";
@@ -40,26 +41,33 @@
consumptionUnit = useMPG ? "US gal" : "l";
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
}
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.GasRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.GasRecord);
}
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Gas Records")}: {Model.GasRecords.Count()}")</span>
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage, "# of Gas Records")}: {Model.GasRecords.Count()}")</span>
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
{
<span class="ms-2 badge bg-primary" id="averageFuelMileageLabel">@($"{translator.Translate(userLanguage,"Average Fuel Economy")}: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}")</span>
<span class="ms-2 badge bg-primary" id="averageFuelMileageLabel">@($"{translator.Translate(userLanguage, "Average Fuel Economy")}: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}")</span>
if (useMPG)
{
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"{translator.Translate(userLanguage,"Min Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary" id="maxFuelMileageLabel">@($"{translator.Translate(userLanguage,"Max Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
} else
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"{translator.Translate(userLanguage, "Min Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary" id="maxFuelMileageLabel">@($"{translator.Translate(userLanguage, "Max Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
}
else
{
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"{translator.Translate(userLanguage,"Min Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary" id="maxFuelMileageLabel">@($"{translator.Translate(userLanguage,"Max Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"{translator.Translate(userLanguage, "Min Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary" id="maxFuelMileageLabel">@($"{translator.Translate(userLanguage, "Max Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
}
}
<span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage,"Total Fuel Consumed")}: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
<span class="ms-2 badge bg-success" data-aggregate-type="sum">@($"{translator.Translate(userLanguage,"Total Cost")}: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
<span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage, "Total Fuel Consumed")}: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
<span class="ms-2 badge bg-success" data-aggregate-type="sum">@($"{translator.Translate(userLanguage, "Total Cost")}: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
@foreach (string recordTag in recordTags)
{
<span onclick="toggleGasFilter(this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
@@ -74,19 +82,82 @@
@if (enableCsvImports)
{
<div class="btn-group">
<button onclick="showAddGasRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Gas Record")</button>
<button onclick="showAddGasRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Gas Record")</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="showBulkImportModal('GasRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('GasRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('GasRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('GasRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchGasTableRows()">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='daterefueled' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_DateRefueled" checked>
<label class="form-check-label stretched-link" for="chkCol_DateRefueled">@translator.Translate(userLanguage, "Date Refueled")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='delta' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Delta" checked>
<label class="form-check-label stretched-link" for="chkCol_Delta">@translator.Translate(userLanguage, "Delta")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='consumption' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Consumption" checked>
<label class="form-check-label stretched-link" for="chkCol_Consumption">@translator.Translate(userLanguage, "Consumption")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='fueleconomy' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_FuelEconomy" checked>
<label class="form-check-label stretched-link" for="chkCol_FuelEconomy">@translator.Translate(userLanguage, "Fuel Economy")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='unitcost' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_UnitCost" checked>
<label class="form-check-label stretched-link" for="chkCol_UnitCost">@translator.Translate(userLanguage, "Unit Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="chkCol_Notes">
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'GasRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
} else {
<button onclick="showAddGasRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Gas Record")</button>
}
else
{
<button onclick="showAddGasRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Gas Record")</button>
}
</div>
</div>
@@ -100,26 +171,36 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2">@translator.Translate(userLanguage,"Date Refueled")</th>
<th scope="col" class="col-2">@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")</th>
<th scope="col" class="col-1" style="cursor:pointer;" onclick="toggleSort('gas-tab-pane', this)">@($"Δ({distanceUnit})")</th>
<th scope="col" class="col-2" data-gas="consumption" data-unit="@consumptionUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{translator.Translate(userLanguage,"Consumption")}({consumptionUnit})")</th>
<th scope="col" class="col-3" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage,"Fuel Economy")}({fuelEconomyUnit})")</th>
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage,"Cost")</th>
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage,"Unit Cost")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="daterefueled">@translator.Translate(userLanguage, "Date Refueled")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="odometer">@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")</th>
<th scope="col" class="col-1 flex-grow-1" data-column="delta" style="cursor:pointer;" onclick="toggleSort('gas-tab-pane', this)">@($"Δ({distanceUnit})")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="consumption" data-gas="consumption" data-unit="@consumptionUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{translator.Translate(userLanguage, "Consumption")}({consumptionUnit})")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="fueleconomy" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")</th>
<th scope="col" class="col-1 flex-grow-1" data-column="cost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-1 flex-grow-1" data-column="unitcost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Unit Cost")</th>
<th scope="col" class="col-3 flex-grow-1" style='display:none;' data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (GasRecordViewModel gasRecord in Model.GasRecords)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)" data-tags='@string.Join(" ", gasRecord.Tags)'>
<td class="col-2">@gasRecord.Date</td>
<td class="col-2" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@gasRecord.Mileage</td>
<td class="col-1">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
<td class="col-2" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
<td class="col-3" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-1" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
<td class="col-1" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@gasRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditGasRecordModal,@gasRecord.Id)" data-tags='@string.Join(" ", gasRecord.Tags)'>
<td class="col-2 flex-grow-1" data-column="daterefueled">@gasRecord.Date</td>
<td class="col-2 flex-grow-1" data-column="odometer" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@gasRecord.Mileage</td>
<td class="col-1 flex-grow-1" data-column="delta">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
<td class="col-2 flex-grow-1" data-column="consumption" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
<td class="col-3 flex-grow-1" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-1 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
<td class="col-1 flex-grow-1" data-column="unitcost" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
<td class="col-3 flex-grow-1 text-truncate" style='display:none;' data-column="notes">@StaticHelper.TruncateStrings(gasRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(gasRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -131,18 +212,28 @@
<div class="modal fade" data-bs-focus="false" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="gasRecordModalContent">
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'GasRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'GasRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}
<script>
@if (!string.IsNullOrWhiteSpace(preferredFuelEconomyUnit))
{
@:convertFuelMileageUnits(decodeHTMLEntities('@fuelEconomyUnit'), decodeHTMLEntities('@preferredFuelEconomyUnit'), false);
}
@if (!string.IsNullOrWhiteSpace(preferredGasUnit))
{
@if (!string.IsNullOrWhiteSpace(preferredGasUnit))
{
@:convertGasConsumptionUnits(decodeHTMLEntities('@consumptionUnit'), decodeHTMLEntities('@preferredGasUnit'), false);
}

View File

@@ -8,7 +8,7 @@
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
var sortedByMPG = Model.OrderBy(x => x.Cost).ToList();
}
@if (Model.Where(x=>x.Cost > 0).Any())
@if (Model.Where(x=>x.Cost > 0).Any() || Model.Where(x=>x.DistanceTraveled > 0).Any())
{
<canvas id="bar-chart"></canvas>
<script>
@@ -16,6 +16,7 @@
function renderChart() {
var barGraphLabels = [];
var barGraphData = [];
var lineChartData = [];
//color gradient from high to low
var barGraphColors = [];
var useDarkMode = getGlobalConfig().useDarkMode;
@@ -23,6 +24,7 @@
{
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
@:lineChartData.push(globalParseFloat('@gasCost.DistanceTraveled'));
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
@:barGraphColors.push('@barGraphColors[index]');
}
@@ -34,7 +36,18 @@
{
label: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")'),
backgroundColor: barGraphColors,
data: barGraphData
data: barGraphData,
order: 1,
yAxisID: 'y',
},
{
label: decodeHTMLEntities('@translator.Translate(userLanguage, "Distance Traveled by Month")'),
data: lineChartData,
borderColor: useDarkMode ? "#fff" : "#000",
backgroundColor: useDarkMode ? "#000" : "#fff",
type: 'line',
order: 0,
yAxisID: 'y1',
}
]
},
@@ -43,7 +56,7 @@
title: {
display: true,
color: useDarkMode ? "#fff" : "#000",
text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")')
text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses and Distance Traveled by Month")')
},
legend: {
display: false,
@@ -59,6 +72,13 @@
color: useDarkMode ? "#fff" : "#000"
}
},
y1: {
beginAtZero: true,
position: 'right',
ticks: {
color: useDarkMode ? "#fff" : "#000"
}
},
x: {
ticks: {
color: useDarkMode ? "#fff" : "#000"

View File

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

View File

@@ -0,0 +1,55 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model GenericRecordEditModel
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title">@translator.Translate(userLanguage,"Edit Multiple Records")</h5>
<button type="button" class="btn-close" onclick="hideGenericRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12">
<label for="genericRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group">
<input type="text" id="genericRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="genericRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" inputmode="numeric" id="genericRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="genericRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="genericRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="genericRecordCost">@translator.Translate(userLanguage, "Cost")</label>
<input type="text" inputmode="decimal" id="genericRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="genericRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
<select multiple class="form-select" id="genericRecordTag"></select>
</div>
<div class="col-md-6 col-12">
<label for="genericRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="genericRecordNotes" class="form-control" rows="5"></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideGenericRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" class="btn btn-primary" onclick="saveGenericRecord()">@translator.Translate(userLanguage,"Edit")</button>
</div>
<script>
var recordsToEdit = [];
@foreach(int recordId in Model.RecordIds)
{
@:recordsToEdit.push(@recordId);
}
function getGenericRecordEditModelData(){
return {
dataType: decodeHTMLEntities('@Model.DataType.ToString()')
}
}
</script>

View File

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

View File

@@ -44,7 +44,7 @@
<tbody>
@foreach (Note note in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)" data-tags='@string.Join(" ", note.Tags)'>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@note.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditNoteModal,@note.Id)" data-tags='@string.Join(" ", note.Tags)'>
@if (note.Pinned)
{
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description</td>
@@ -66,4 +66,16 @@
<div class="modal-content" id="noteModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, true)">@translator.Translate(userLanguage, "Toggle Pin")</a></li>
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, false, true)">@translator.Translate(userLanguage, "Pin")</a></li>
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, false, false)">@translator.Translate(userLanguage, "Unpin")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'NoteRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'NoteRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>

View File

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

View File

@@ -7,12 +7,18 @@
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var userLanguage = userConfig.UserLanguage;
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x=>x.Tab == ImportMode.OdometerRecord);
}
@model List<OdometerRecord>
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Odometer Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage, "# of Odometer Records")}: {Model.Count()}")</span>
@foreach (string recordTag in recordTags)
{
<span onclick="filterTable('odometer-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
@@ -28,21 +34,52 @@
@if (enableCsvImports)
{
<div class="btn-group">
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Odometer Record")</button>
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Odometer Record")</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="showBulkImportModal('OdometerRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('OdometerRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('odometer-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'OdometerRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
else
{
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Odometer Record")</button>
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Odometer Record")</button>
}
</div>
</div>
@@ -57,18 +94,26 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage,"Odometer")</th>
<th scope="col" class="col-7 col-xl-8">@translator.Translate(userLanguage,"Notes")</th>
<th scope="col" class="col-2 col-xl-1 flex-grow-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-7 col-xl-8 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (OdometerRecord odometerRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditOdometerRecordModal(@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
<td class="col-2 col-xl-1">@odometerRecord.Date.ToShortDateString()</td>
<td class="col-3" data-record-type="cost">@odometerRecord.Mileage</td>
<td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@odometerRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditOdometerRecordModal,@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1" data-column="date">@odometerRecord.Date.ToShortDateString()</td>
<td class="col-3 flex-grow-1" data-column="odometer" data-record-type="cost">@odometerRecord.Mileage</td>
<td class="col-7 col-xl-8 flex-grow-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(odometerRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -82,4 +127,16 @@
<div class="modal-content" id="odometerRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'OdometerRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'OdometerRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -16,7 +16,13 @@
</div>
</div>
<div class="row">
<div class="col-6 col-md-1">
@if (Model.ReminderRecordId != default)
{
<div class="col-4 col-md-1">
<i class="bi bi-bell"></i>
</div>
}
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
@if (Model.ImportMode == ImportMode.ServiceRecord)
{
<i class="bi bi-card-checklist"></i>
@@ -30,7 +36,7 @@
<i class="bi bi-exclamation-octagon"></i>
}
</div>
<div class="col-6 col-md-1">
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
@if (Model.Priority == PlanPriority.Critical)
{
<i class="bi bi-fire"></i>

View File

@@ -65,14 +65,14 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="planRecordFiles">@translator.Translate(userLanguage, "Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
</div>
}
else
{
<label for="planRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="planRecordFiles">
<br />
<small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
@@ -85,6 +85,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
}
<button type="button" class="btn btn-danger" onclick="deletePlanRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddPlanRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
@@ -97,7 +101,10 @@
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="savePlanRecordTemplate()">@translator.Translate(userLanguage, "Save as Template")</a></li>
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
@if (!Model.CreatedFromReminder)
{
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
}
</ul>
</div>
}
@@ -106,6 +113,7 @@
<button type="button" class="btn btn-primary" onclick="savePlanRecordToVehicle(true)">@translator.Translate(userLanguage, "Edit Plan Record")</button>
}
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
var selectedSupplies = [];
@@ -119,7 +127,9 @@
function getPlanRecordModelData() {
return {
id: @Model.Id,
dateCreated: decodeHTMLEntities('@(Model.DateCreated)')
dateCreated: decodeHTMLEntities('@(Model.DateCreated)'),
reminderRecordId: decodeHTMLEntities('@Model.ReminderRecordId'),
createdFromReminder: @Model.CreatedFromReminder.ToString().ToLower()
}
}
</script>

View File

@@ -29,6 +29,10 @@
<tr class="d-flex" id="supplyRows">
<td class="col-8 text-truncate">
@StaticHelper.TruncateStrings(planRecordTemplate.Description)
@if(planRecordTemplate.ReminderRecordId != default)
{
<i class="bi bi-bell ms-2"></i>
}
@if (planRecordTemplate.Files.Any())
{
<i class="bi bi-paperclip ms-2"></i>

View File

@@ -0,0 +1,13 @@
@model List<ReminderRecord>
<select class="form-select" id="recurringReminderInput">
@if (Model.Any())
{
@foreach (ReminderRecord reminderRecord in Model)
{
<!option value="@reminderRecord.Id">@reminderRecord.Description</!option>
}
} else
{
<!option value="0">No Recurring Reminders Found</!option>
}
</select>

View File

@@ -35,7 +35,7 @@
<div class="input-group">
<input type="number" inputmode="numeric" id="reminderMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Future Odometer Reading")" value="@(isNew ? "" : Model.Mileage)">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary" onclick="appendMileageToOdometer(500)">+500</button>
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="appendMileageToOdometer(500)">+500</button>
</div>
</div>
<div class="form-check form-check-inline">
@@ -90,6 +90,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.IsRecurring)
{
<button type="button" class="btn btn-warning" onclick="createPlanRecordFromReminder(@Model.Id)">@translator.Translate(userLanguage, "Plan")</button>
}
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddReminderRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>

View File

@@ -45,7 +45,7 @@
<tbody>
@foreach (ReminderRecordViewModel reminderRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditReminderRecordModal(@reminderRecord.Id)">
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@reminderRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditReminderRecordModal,@reminderRecord.Id)">
@if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent)
{
<td class="col-1"><span class="badge text-bg-danger">@translator.Translate(userLanguage, "Very Urgent")</span></td>
@@ -93,4 +93,18 @@
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="planRecordModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="planRecordModalContent">
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>

View File

@@ -29,26 +29,55 @@
<div class="col-md-6 col-12 mt-2">
<div class="row">
<div class="col-md-1 d-sm-none d-md-block"></div>
<div class="col-12 col-md-10 reportsCheckBoxContainer">
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
<label class="form-check-label" for="serviceExpenseCheck">@translator.Translate(userLanguage,"Service")</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
<label class="form-check-label" for="repairExpenseCheck">@translator.Translate(userLanguage, "Repairs")</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
<label class="form-check-label" for="upgradeExpenseCheck">@translator.Translate(userLanguage, "Upgrades")</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
<label class="form-check-label" for="gasExpenseCheck">@translator.Translate(userLanguage, "Fuel")</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
<label class="form-check-label" for="taxExpenseCheck">@translator.Translate(userLanguage, "Taxes")</label>
<div class="col-12 col-md-10">
<div class="dropdown d-grid dropdown-center">
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Metrics
</button>
<ul class="dropdown-menu" style="width:100%;">
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input" onChange="updateCheckAll()" type="checkbox" id="selectAllExpenseCheck" value="0" checked>
<label class="form-check-label stretched-link" for="selectAllExpenseCheck">@translator.Translate(userLanguage, "Select All")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="odometerExpenseCheck" value="6" checked>
<label class="form-check-label stretched-link" for="odometerExpenseCheck">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
<label class="form-check-label stretched-link" for="serviceExpenseCheck">@translator.Translate(userLanguage, "Service")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
<label class="form-check-label stretched-link" for="repairExpenseCheck">@translator.Translate(userLanguage, "Repairs")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
<label class="form-check-label stretched-link" for="upgradeExpenseCheck">@translator.Translate(userLanguage, "Upgrades")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
<label class="form-check-label stretched-link" for="gasExpenseCheck">@translator.Translate(userLanguage, "Fuel")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
<label class="form-check-label stretched-link" for="taxExpenseCheck">@translator.Translate(userLanguage, "Taxes")</label>
</div>
</li>
</ul>
</div>
</div>
<div class="col-md-1 d-sm-none d-md-block"></div>

View File

@@ -26,6 +26,14 @@
<input type="number" inputmode="numeric" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew ? "" : Model.Mileage)">
<label for="serviceRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="serviceRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) serviced(i.e. Oil Change)")" value="@Model.Description">
@if (isNew)
{
<div class="row">
<div class="col-12">
<a onclick="showRecurringReminderSelector('serviceRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div>
</div>
}
<label for="serviceRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" inputmode="decimal" id="serviceRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the service")" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
@@ -56,7 +64,7 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="serviceRecordFiles">@translator.Translate(userLanguage,"Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div>
}
@@ -72,7 +80,7 @@
</div>
}
<label for="serviceRecordFiles">@translator.Translate(userLanguage,"Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
</div>
@@ -83,6 +91,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
}
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteServiceRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
@@ -105,9 +117,11 @@
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Service Record")</button>
}
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
var selectedSupplies = [];
var recurringReminderRecordId = 0;
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)

View File

@@ -7,14 +7,20 @@
var hideZero = userConfig.HideZero;
var userLanguage = userConfig.UserLanguage;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.ServiceRecord);
}
@model List<ServiceRecord>
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Service Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary" data-aggregate-type="sum">@($"{translator.Translate(userLanguage,"Total")}: {Model.Sum(x => x.Cost).ToString("C")}")</span>
@foreach(string recordTag in recordTags)
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage, "# of Service Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary" data-aggregate-type="sum">@($"{translator.Translate(userLanguage, "Total")}: {Model.Sum(x => x.Cost).ToString("C")}")</span>
@foreach (string recordTag in recordTags)
{
<span onclick="filterTable('servicerecord-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
}
@@ -29,21 +35,64 @@
@if (enableCsvImports)
{
<div class="btn-group">
<button onclick="showAddServiceRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Service Record")</button>
<button onclick="showAddServiceRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Service Record")</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="showBulkImportModal('ServiceRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('ServiceRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('ServiceRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('ServiceRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('servicerecord-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'ServiceRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
else
{
<button onclick="showAddServiceRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Service Record")</button>
<button onclick="showAddServiceRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Service Record")</button>
}
</div>
</div>
@@ -58,22 +107,30 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2" onclick="toggleSort('servicerecord-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
<th scope="col" class="col-2 col-xl-1 flex-grow-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4 flex-grow-1" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="cost" onclick="toggleSort('servicerecord-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (ServiceRecord serviceRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditServiceRecordModal(@serviceRecord.Id)" data-tags='@string.Join(" ",serviceRecord.Tags)'>
<td class="col-2 col-xl-1">@serviceRecord.Date.ToShortDateString()</td>
<td class="col-2">@serviceRecord.Mileage</td>
<td class="col-3 col-xl-4">@serviceRecord.Description</td>
<td class="col-2" data-record-type="cost">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(serviceRecord.Notes)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@serviceRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditServiceRecordModal,@serviceRecord.Id)" data-tags='@string.Join(" ", serviceRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(serviceRecord.Date)">@serviceRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1" data-column="odometer">@serviceRecord.Mileage</td>
<td class="col-3 col-xl-4 flex-grow-1" data-column="description">@serviceRecord.Description</td>
<td class="col-2 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate flex-grow-1" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(serviceRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(serviceRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -85,7 +142,25 @@
<div class="modal fade" data-bs-focus="false" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="serviceRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'ServiceRecord', 'RepairRecord')">@translator.Translate(userLanguage, "Repairs")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'ServiceRecord', 'UpgradeRecord')">@translator.Translate(userLanguage, "Upgrades")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)">@translator.Translate(userLanguage, "Statistics")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -31,7 +31,12 @@
<div class="row">
<div class="col-md-6 col-12">
<label for="supplyRecordQuantity">@translator.Translate(userLanguage,"Quantity")</label>
<input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
<div class="input-group">
<input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
<div class="input-group-text">
<button type="button" class="btn btn-sm zero-y-padding btn-primary" onclick="replenishSupplies()">+</button>
</div>
</div>
</div>
<div class="col-md-6 col-12">
<label for="supplyRecordCost">@translator.Translate(userLanguage,"Cost")</label>
@@ -62,14 +67,14 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="supplyRecordFiles">@translator.Translate(userLanguage,"Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div>
}
else
{
<label for="supplyRecordFiles">@translator.Translate(userLanguage,"Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
</div>
@@ -80,6 +85,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-clock-history"></i></button>
}
<button type="button" class="btn btn-danger" onclick="deleteSupplyRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddSupplyRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@@ -92,15 +101,16 @@
<button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Supply Record")</button>
}
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
function getSupplyRecordModelData() {
return { id: @Model.Id}
}

View File

@@ -7,6 +7,12 @@
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.SupplyRecord);
}
@model List<SupplyRecord>
<div class="row">
@@ -38,6 +44,61 @@
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('SupplyRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('supply-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='partnumber' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_PartNumber" checked>
<label class="form-check-label stretched-link" for="chkCol_PartNumber">@translator.Translate(userLanguage, "Part Number")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='supplier' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Supplier" checked>
<label class="form-check-label stretched-link" for="chkCol_Supplier">@translator.Translate(userLanguage, "Supplier")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='quantity' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Quantity" checked>
<label class="form-check-label stretched-link" for="chkCol_Quantity">@translator.Translate(userLanguage, "Quantity")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'SupplyRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
@@ -58,26 +119,34 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part #")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Supplier")</th>
<th scope="col" class="col-2 col-xl-3">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-1" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-1" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Notes")</th>
<th scope="col" class="col-2 flex-grow-1 col-xl-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="partnumber">@translator.Translate(userLanguage, "Part #")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="supplier">@translator.Translate(userLanguage, "Supplier")</th>
<th scope="col" class="col-2 flex-grow-1 col-xl-3" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-1 flex-grow-1" data-column="quantity" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-1 flex-grow-1" data-column="cost" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (SupplyRecord supplyRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditSupplyRecordModal(@supplyRecord.Id)" data-tags='@string.Join(" ", supplyRecord.Tags)'>
<td class="col-2 col-xl-1">@supplyRecord.Date.ToShortDateString()</td>
<td class="col-2">@supplyRecord.PartNumber</td>
<td class="col-2">@supplyRecord.PartSupplier</td>
<td class="col-2 col-xl-3">@supplyRecord.Description</td>
<td class="col-1">@supplyRecord.Quantity</td>
<td class="col-1" data-record-type="cost">@((hideZero && supplyRecord.Cost == default) ? "---" : supplyRecord.Cost.ToString("C"))</td>
<td class="col-2 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(supplyRecord.Notes)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@supplyRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditSupplyRecordModal,@supplyRecord.Id)" data-tags='@string.Join(" ", supplyRecord.Tags)'>
<td class="col-2 flex-grow-1 col-xl-1" data-column="date">@supplyRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1" data-column="partnumber">@supplyRecord.PartNumber</td>
<td class="col-2 flex-grow-1" data-column="supplier">@supplyRecord.PartSupplier</td>
<td class="col-2 flex-grow-1 col-xl-3" data-column="description">@supplyRecord.Description</td>
<td class="col-1 flex-grow-1" data-column="quantity">@supplyRecord.Quantity</td>
<td class="col-1 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && supplyRecord.Cost == default) ? "---" : supplyRecord.Cost.ToString("C"))</td>
<td class="col-2 flex-grow-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(supplyRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(supplyRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -91,4 +160,16 @@
<div class="modal-content" id="supplyRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'SupplyRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'SupplyRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -0,0 +1,72 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<SupplyUsageHistory>
<script>
var supplyUsageHistory = [];
</script>
<div id="supplyUsageHistoryModalContainer" class="d-none">
<div class="modal-header">
<h5 class="modal-title">@translator.Translate(userLanguage, "Supply Requisition History")</h5>
</div>
<div class="modal-body">
@if (Model.Any())
{
<div class="row">
<div class="col-12" style="max-height:50vh; overflow-y:auto;">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Date")</th>
@if(Model.Any(x=>!string.IsNullOrWhiteSpace(x.PartNumber))){
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part Number")</th>
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Description")</th>
} else
{
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Description")</th>
}
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Cost")</th>
</tr>
</thead>
<tbody>
@foreach (SupplyUsageHistory usageHistory in Model)
{
<script>
supplyUsageHistory.push({ date: decodeHTMLEntities("@usageHistory.Date.ToShortDateString()"), partNumber: decodeHTMLEntities('@usageHistory.PartNumber'), description: decodeHTMLEntities("@usageHistory.Description"), quantity: decodeHTMLEntities("@usageHistory.Quantity.ToString("F")"), cost: decodeHTMLEntities("@usageHistory.Cost.ToString("F")") })
</script>
<tr class="d-flex">
<td class="col-2">@StaticHelper.TruncateStrings(usageHistory.Date.ToShortDateString())</td>
@if (!string.IsNullOrWhiteSpace(usageHistory.PartNumber))
{
<td class="col-2 text-truncate">@StaticHelper.TruncateStrings(usageHistory.PartNumber)</td>
<td class="col-4 text-truncate">@StaticHelper.TruncateStrings(usageHistory.Description)</td>
} else
{
<td class="col-6 text-truncate">@StaticHelper.TruncateStrings(usageHistory.Description, 50)</td>
}
<td class="col-2">@usageHistory.Quantity.ToString("F")</td>
<td class="col-2">@usageHistory.Cost.ToString("C2")</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
else
{
<div class="row">
<div class="col-12">
<div class="text-center">
<h4>@translator.Translate(userLanguage, "No supply requisitions in history")</h4>
</div>
</div>
</div>
}
</div>
</div>

View File

@@ -24,6 +24,14 @@
</div>
<label for="taxRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="taxRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of tax paid(i.e. Registration)")" value="@Model.Description">
@if (isNew)
{
<div class="row">
<div class="col-12">
<a onclick="showRecurringReminderSelector('taxRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div>
</div>
}
<label for="taxRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" inputmode="decimal" id="taxRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of tax paid")" value="@(isNew? "" : Model.Cost)">
<label for="taxRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
@@ -65,7 +73,7 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="taxRecordFiles">@translator.Translate(userLanguage,"Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div>
}
@@ -81,7 +89,7 @@
</div>
}
<label for="taxRecordFiles">@translator.Translate(userLanguage,"Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
</div>
@@ -107,6 +115,7 @@
<script>
var uploadedFiles = [];
var customMonthInterval = @Model.CustomMonthInterval;
var recurringReminderRecordId = 0;
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)

View File

@@ -7,6 +7,12 @@
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.TaxRecord);
}
@model List<TaxRecord>
<div class="row">
@@ -38,6 +44,43 @@
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('TaxRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('tax-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'TaxRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
@@ -58,20 +101,28 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-3 col-xl-1">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-4 col-xl-6">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2" onclick="toggleSort('tax-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
<th scope="col" class="col-3 flex-grow-1 col-xl-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-4 flex-grow-1 col-xl-6" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="cost" onclick="toggleSort('tax-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (TaxRecord taxRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)" data-tags='@string.Join(" ", taxRecord.Tags)'>
<td class="col-3 col-xl-1">@taxRecord.Date.ToShortDateString()</td>
<td class="col-4 col-xl-6">@taxRecord.Description</td>
<td class="col-2" data-record-type="cost">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(taxRecord.Notes)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@taxRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditTaxRecordModal,@taxRecord.Id)" data-tags='@string.Join(" ", taxRecord.Tags)'>
<td class="col-3 flex-grow-1 col-xl-1" data-column="date">@taxRecord.Date.ToShortDateString()</td>
<td class="col-4 flex-grow-1 col-xl-6" data-column="description">@taxRecord.Description</td>
<td class="col-2 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(taxRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(taxRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -85,4 +136,16 @@
<div class="modal-content" id="taxRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'TaxRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'TaxRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -26,6 +26,14 @@
<input type="number" inputmode="numeric" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew ? "" : Model.Mileage)">
<label for="upgradeRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) upgraded/modded")" value="@Model.Description">
@if (isNew)
{
<div class="row">
<div class="col-12">
<a onclick="showRecurringReminderSelector('upgradeRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
</div>
</div>
}
<label for="upgradeRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" inputmode="decimal" id="upgradeRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the upgrade/mods")" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
@@ -56,7 +64,7 @@
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="upgradeRecordFiles">@translator.Translate(userLanguage,"Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div>
}
@@ -72,7 +80,7 @@
</div>
}
<label for="upgradeRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
}
</div>
@@ -83,6 +91,10 @@
<div class="modal-footer">
@if (!isNew)
{
@if (Model.RequisitionHistory.Any())
{
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
}
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteUpgradeRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
@@ -105,9 +117,11 @@
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Upgrade Record")</button>
}
</div>
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
<script>
var uploadedFiles = [];
var selectedSupplies = [];
var recurringReminderRecordId = 0;
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)

View File

@@ -7,6 +7,12 @@
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var extraFields = new List<string>();
if (userConfig.EnableExtraFieldColumns)
{
extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList();
}
var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.UpgradeRecord);
}
@model List<UpgradeRecord>
<div class="row">
@@ -38,6 +44,49 @@
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('UpgradeRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
<li><a class="dropdown-item" href="#" onclick="searchTableRows('upgrade-tab-pane')">@translator.Translate(userLanguage, "Search")</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Visible Columns")</h6></li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='date' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Date" checked>
<label class="form-check-label stretched-link" for="chkCol_Date">@translator.Translate(userLanguage, "Date")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='odometer' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Odometer" checked>
<label class="form-check-label stretched-link" for="chkCol_Odometer">@translator.Translate(userLanguage, "Odometer")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='description' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Description" checked>
<label class="form-check-label stretched-link" for="chkCol_Description">@translator.Translate(userLanguage, "Description")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='cost' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Cost" checked>
<label class="form-check-label stretched-link" for="chkCol_Cost">@translator.Translate(userLanguage, "Cost")</label>
</div>
</li>
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='notes' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="chkCol_Notes" checked>
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
</div>
</li>
@foreach (string extraFieldColumn in extraFields)
{
var elementId = Guid.NewGuid();
<li class="dropdown-item">
<div class="list-group-item">
<input class="form-check-input col-visible-toggle" data-column-toggle='@extraFieldColumn' onChange="showTableColumns(this, 'UpgradeRecord')" type="checkbox" id="@elementId">
<label class="form-check-label stretched-link" for="@elementId">@extraFieldColumn</label>
</div>
</li>
}
</ul>
</div>
}
@@ -58,22 +107,30 @@
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
<th scope="col" class="col-2 flex-grow-1 col-xl-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 flex-grow-1 col-xl-4" data-column="description">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2 flex-grow-1" data-column="cost" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-3 flex-grow-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
@foreach (string extraFieldColumn in extraFields)
{
<th scope="col" style='display:none;' class="col-2 flex-grow-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
}
</tr>
</thead>
<tbody>
@foreach (UpgradeRecord upgradeRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
<td class="col-2 col-xl-1">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2">@upgradeRecord.Mileage</td>
<td class="col-3 col-xl-4">@upgradeRecord.Description</td>
<td class="col-2" data-record-type="cost">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@upgradeRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditUpgradeRecordModal,@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
<td class="col-2 flex-grow-1 col-xl-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(upgradeRecord.Date)">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1" data-column="odometer">@upgradeRecord.Mileage</td>
<td class="col-3 flex-grow-1 col-xl-4" data-column="description">@upgradeRecord.Description</td>
<td class="col-2 flex-grow-1" data-column="cost" data-record-type="cost">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
@foreach (string extraFieldColumn in extraFields)
{
<td class="col-2 flex-grow-1 text-truncate" style='display:none;' data-column="@extraFieldColumn">@(upgradeRecord.ExtraFields.Where(x => x.Name == extraFieldColumn)?.FirstOrDefault()?.Value ?? "")</td>
}
</tr>
}
</tbody>
@@ -86,4 +143,23 @@
<div class="modal-content" id="upgradeRecordModalContent">
</div>
</div>
</div>
</div>
<ul class="table-context-menu dropdown-menu" style="display:none;">
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
<li><hr class="context-menu-multiple dropdown-divider"></li>
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'UpgradeRecord', 'ServiceRecord')">@translator.Translate(userLanguage, "Service Records")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'UpgradeRecord', 'RepairRecord')">@translator.Translate(userLanguage, "Repairs")</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
<li><hr class="context-menu-active-multiple dropdown-divider"></li>
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="getRecordsDeltaStats(selectedRow)">@translator.Translate(userLanguage, "Statistics")</a></li>
</ul>
@if (userColumnPreferences.Any())
{
@await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences)
}

View File

@@ -0,0 +1,9 @@
@model IEnumerable<UserColumnPreference>
<script>
var visibleColumns = [];
@foreach(string visibleColumn in Model.SelectMany(x=> x.VisibleColumns))
{
@:visibleColumns.push(decodeHTMLEntities('@visibleColumn'));
}
loadUserColumnPreferences(visibleColumns);
</script>

View File

@@ -25,14 +25,30 @@
<span class="lead">@Model.VehicleData.LicensePlate</span>
</li>
<li class="list-group-item">
@if (Model.VehicleData.IsElectric)
{
<span><i class="bi bi-ev-station me-2"></i>@translator.Translate(userLanguage, "Electric")</span>
}
else
{
<span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span>
}
<div class="row">
<div class="col-4">
@if (Model.VehicleData.IsElectric)
{
<span><i class="bi bi-ev-station me-2"></i>@translator.Translate(userLanguage, "Electric")</span>
}
else
{
<span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span>
}
</div>
@if (!string.IsNullOrWhiteSpace(Model.DaysOwned))
{
<div class="col-4">
<span><i class="bi bi-calendar-range me-2"></i>@($"{Model.DaysOwned} {translator.Translate(userLanguage, "Days")}")</span>
</div>
}
@if (Model.DistanceTraveled != default)
{
<div class="col-4">
<span><i class="bi bi-speedometer me-2"></i>@($"{Model.DistanceTraveled} {Model.DistanceUnit}")</span>
</div>
}
</div>
</li>
</ul>
</div>
@@ -40,8 +56,8 @@
<ul class="list-group">
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Last Reported Odometer Reading")}: {Model.Odometer}") </li>
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Average Fuel Economy")}: {Model.MPG}") </li>
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent(excl. fuel)")}: {Model.TotalCost.ToString("C")}") </li>
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent on Fuel")}: {Model.TotalGasCost.ToString("C")}") </li>
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent(excl. fuel)")}: {Model.TotalCost.ToString("C")} ({Model.TotalCostPerMile.ToString("C")}/{Model.DistanceUnit})") </li>
<li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent on Fuel")}: {Model.TotalGasCost.ToString("C")} ({Model.TotalGasCostPerMile.ToString("C")}/{Model.DistanceUnit})") </li>
</ul>
</div>
</div>

View File

@@ -34,6 +34,10 @@
<input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model">
<label for="inputLicensePlate">@translator.Translate(userLanguage, "License Plate")</label>
<input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
<label for="inputPurchaseDate">@translator.Translate(userLanguage, "Purchased Date(optional)")</label>
<input type="text" id="inputPurchaseDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Date")" value="@Model.PurchaseDate">
<label for="inputSoldDate">@translator.Translate(userLanguage, "Sold Date(optional)")</label>
<input type="text" id="inputSoldDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Sold Date")" value="@Model.SoldDate">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
<label class="form-check-label" for="inputIsElectric">@translator.Translate(userLanguage, "Electric Vehicle")</label>
@@ -49,6 +53,14 @@
<!option value="@tag">@tag</!option>
}
</select>
@foreach (ExtraField field in Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
{
<label for="inputImage">@translator.Translate(userLanguage, "Replace picture(optional)")</label>

View File

@@ -15,10 +15,13 @@
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
"EnableShopSupplies": false,
"EnableExtraFieldColumns": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,
"UseMarkDownOnSavedNotes": false,
"HideSoldVehicles": false,
"PreferredGasMileageUnit": "",
"UserColumnPreferences": [],
"PreferredGasUnit": "",
"UserLanguage": "en_US",
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],

View File

@@ -0,0 +1,45 @@
---
version: "3.4"
services:
app:
image: ghcr.io/hargata/lubelogger:latest
build: .
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- temp:/App/wwwroot/temp
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
- 8080:8080
env_file:
- .env
postgres:
image: postgres:14
restart: unless-stopped
environment:
POSTGRES_USER: "lubelogger"
POSTGRES_PASSWORD: "lubepass"
POSTGRES_DB: "lubelogger"
volumes:
- postgres:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
volumes:
config:
data:
translations:
documents:
images:
temp:
log:
keys:
postgres:

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -2,6 +2,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="favicon.ico">
<title>LubeLogger</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<style>
@@ -30,7 +31,7 @@
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#features">Features</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#demo">Demo</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#download">Download</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://docs.lubelogger.com">Docs</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://docs.lubelogger.com">Documentation</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://github.com/hargata/lubelog">GitHub Repo</a></div>
</div>
<hr>
@@ -42,12 +43,12 @@
<div class="row">
<div id="carouselGallery" class="carousel slide">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="0" class="active" aria-current="true"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="1"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="2"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="3"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="4"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="5"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="0" class="active" aria-current="true"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="1"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="2"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="3"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="4"></button>
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="5"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
@@ -94,11 +95,11 @@
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselGallery" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="carousel-control-prev-icon" style='filter:inherit;' aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselGallery" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="carousel-control-next-icon" style='filter:inherit;' aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
@@ -141,7 +142,7 @@
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Live demo available <a href="https://demo.lubelogger.com">here</a>
Live demo available <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://demo.lubelogger.com">here</a>
</p>
</div>
<div class="col-12 d-flex justify-content-center">
@@ -151,44 +152,20 @@
<hr>
<div class="row" id="download">
<div class="col-12 d-flex justify-content-center">
<h6 class="display-6 text-center">Where to Download</h6>
<h6 class="display-6 text-center">Download</h6>
</div>
</div>
<div class="row">
<div class="col-12 d-flex justify-content-center">
<p class="lead">
LubeLogger is released exclusively as a Docker Image via the GitHub Container Repository(GHCR)
LubeLogger is available as both a Docker Image and a Windows Standalone Executable(EXE)
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
To pull down the Docker Image, run
Read this <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://docs.lubelogger.com/Getting%20Started'>Getting Started Guide</a> on how to download either of them
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p><code>docker pull ghcr.io/hargata/lubelogger:latest</code></p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Check the <a href="https://github.com/hargata/lubelog/" target="_blank">GitHub repository</a> and clone the "docker-compose.yml" and ".env" file onto your computer.
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Navigate to the directory where those two files are located, then run
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p><code>docker-compose up</code></p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Navigate to the URL the service is listening on, the default is
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p><code>http://localhost:8080</code></p>
</div>
</div>
<hr>
<div class="row">
@@ -196,15 +173,11 @@
<h6 class="display-6 text-center">About</h6>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">LubeLogger is proudly developed in Price Utah by Hargata Softworks.
<p class="lead">LubeLogger is proudly developed in <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://www.visitutah.com/Places-To-Go/Cities-and-Towns/Price" target="_blank">Price Utah</a> by <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='mailto:hargatasoftworks@gmail.com'>Hargata Softworks</a>
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead"><a href="https://www.patreon.com/LubeLogger">Support us on Patreon</a>
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">For more information regarding Price Utah, click <a href="https://www.visitutah.com/Places-To-Go/Cities-and-Towns/Price" target="_blank">here</a>
<p class="lead"><a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://www.patreon.com/LubeLogger">Support us on Patreon</a> or <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://buy.stripe.com/aEU9Egc8DdMc9bO144'>Donate on Stripe</a>
</p>
</div>
</div>

View File

@@ -163,6 +163,31 @@ html {
transform-origin: top center;
}
.tablerow-shake {
animation: tablerowshake 1.2s cubic-bezier(.36, .07, .19, .97) both;
backface-visibility: hidden;
transform: translate3d(0, 0, 0);
}
@keyframes tablerowshake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
@keyframes bellshake {
0% {
transform: rotate(0);
@@ -270,6 +295,10 @@ html {
z-index: 1030;
}
.table-context-menu {
z-index: 1030;
}
input[type="file"] {
max-width: 100%;
}
@@ -304,7 +333,37 @@ input[type="file"] {
.override-hide{
display: none !important;
}
.reminderCalendarViewContent + .datepicker, .datepicker-inline, .datepicker-days, .table-condensed {
.reminderCalendarViewContent .datepicker, .reminderCalendarViewContent .datepicker-inline, .reminderCalendarViewContent .datepicker-days, .reminderCalendarViewContent .table-condensed {
width: 100%;
height: 100%;
table-layout: fixed;
cursor: default;
}
.reminder-exist{
overflow:auto;
vertical-align:top;
}
.reminder-exist p{
margin:0;
}
.reminderCalendarViewContent .datepicker table tr td.day {
cursor: default;
}
.reminderCalendarViewContent .datepicker table tr td.day:hover {
cursor: default;
}
.reminder-calendar-item{
cursor: pointer;
}
.zero-y-padding{
padding-top: 0rem;
padding-bottom: 0rem;
}
.vehicle-sold-banner {
color: #fff;
background-color: rgba(0, 0, 0, 0.6);
width: 100%;
text-align: center;
position: absolute;
z-index: 10;
}

File diff suppressed because one or more lines are too long

View File

@@ -129,6 +129,8 @@ function getAndValidateCollisionRecordValues() {
supplies: selectedSupplies,
tags: collisionTags,
addReminderRecord: addReminderRecord,
extraFields: extraFields.extraFields
extraFields: extraFields.extraFields,
requisitionHistory: supplyUsageHistory,
reminderRecordId: recurringReminderRecordId
}
}

View File

@@ -4,6 +4,8 @@
if (data) {
$("#addVehicleModalContent").html(data);
initTagSelector($("#inputTag"));
initDatePicker($('#inputPurchaseDate'));
initDatePicker($('#inputSoldDate'));
$('#addVehicleModal').modal('show');
}
})
@@ -62,30 +64,86 @@ function getVehicleCalendarEvents() {
}
});
}
function generateReminderItem(urgency, description) {
function showCalendarReminderModal(id) {
event.stopPropagation();
$.get(`/Home/ViewCalendarReminder?reminderId=${id}`, function (data) {
if (data) {
$("#reminderRecordCalendarModalContent").html(data);
$("#reminderRecordCalendarModal").modal('show');
$('#reminderRecordCalendarModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("reminderNotes");
}
});
}
})
}
function hideCalendarReminderModal() {
$("#reminderRecordCalendarModal").modal('hide');
}
function generateReminderItem(id, urgency, description) {
if (description.trim() == '') {
return;
}
switch (urgency) {
case "VeryUrgent":
return `<p class="badge text-wrap bg-danger">${description}</p>`;
return `<p class="badge text-wrap bg-danger reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
case "PastDue":
return `<p class="badge text-wrap bg-secondary">${description}</p>`;
return `<p class="badge text-wrap bg-secondary reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
case "Urgent":
return `<p class="badge text-wrap bg-warning">${description}</p>`;
return `<p class="badge text-wrap bg-warning reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
case "NotUrgent":
return `<p class="badge text-wrap bg-success">${description}</p>`;
return `<p class="badge text-wrap bg-success reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
}
}
function markDoneCalendarReminderRecord(reminderRecordId, e) {
event.stopPropagation();
$.post(`/Vehicle/PushbackRecurringReminderRecord?reminderRecordId=${reminderRecordId}`, function (data) {
if (data) {
hideCalendarReminderModal();
successToast("Reminder Updated");
getVehicleCalendarEvents();
} else {
errorToast(genericErrorMessage());
}
});
}
function deleteCalendarReminderRecord(reminderRecordId, e) {
if (e != undefined) {
event.stopPropagation();
}
$("#workAroundInput").show();
Swal.fire({
title: "Confirm Deletion?",
text: "Deleted Reminders cannot be restored.",
showCancelButton: true,
confirmButtonText: "Delete",
confirmButtonColor: "#dc3545"
}).then((result) => {
if (result.isConfirmed) {
$.post(`/Vehicle/DeleteReminderRecordById?reminderRecordId=${reminderRecordId}`, function (data) {
if (data) {
hideCalendarReminderModal();
successToast("Reminder Deleted");
getVehicleCalendarEvents();
} else {
errorToast(genericErrorMessage());
}
});
} else {
$("#workAroundInput").hide();
}
});
}
function initCalendar() {
if (groupedDates.length == 0) {
//group dates
eventDates.map(x => {
var existingIndex = groupedDates.findIndex(y => y.date.getTime() == x.date.getTime());
var existingIndex = groupedDates.findIndex(y => y.date == x.date);
if (existingIndex == -1) {
groupedDates.push({ date: x.date, reminders: [`${generateReminderItem(x.urgency, x.description)}`] });
groupedDates.push({ date: x.date, reminders: [`${generateReminderItem(x.id, x.urgency, x.description)}`] });
} else if (existingIndex > -1) {
groupedDates[existingIndex].reminders.push(`${generateReminderItem(x.urgency, x.description)}`);
groupedDates[existingIndex].reminders.push(`${generateReminderItem(x.id, x.urgency, x.description)}`);
}
});
}
@@ -93,13 +151,14 @@ function initCalendar() {
startDate: "+0d",
format: getShortDatePattern().pattern,
todayHighlight: true,
weekStart: getGlobalConfig().firstDayOfWeek,
beforeShowDay: function (date) {
var reminderDateIndex = groupedDates.findIndex(x => x.date.getTime() == date.getTime());
var reminderDateIndex = groupedDates.findIndex(x => (x.date == date.getTime() || x.date == (date.getTime() - date.getTimezoneOffset() * 60000))); //take into account server timezone offset
if (reminderDateIndex > -1) {
return {
enabled: true,
classes: 'reminder-exist',
content: `<div class='text-wrap'><p>${date.getDate()}</p>${groupedDates[reminderDateIndex].reminders.join('<br>')}</div>`
content: `<div class='text-wrap' style='height:20px;'><p>${date.getDate()}</p>${groupedDates[reminderDateIndex].reminders.join('<br>')}</div>`
}
}
}
@@ -114,27 +173,8 @@ function performLogOut() {
}
function loadPinnedNotes(vehicleId) {
var hoveredGrid = $(`#gridVehicle_${vehicleId}`);
if (hoveredGrid.attr("data-bs-title") == undefined) {
$.get(`/Vehicle/GetPinnedNotesByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data.length > 0) {
//converted pinned notes to html.
var htmlString = "<ul class='list-group list-group-flush'>";
data.forEach(x => {
htmlString += `<li><b>${x.description}</b> : ${x.noteText}</li>`;
});
htmlString += "</ul>";
hoveredGrid.attr("data-bs-title", htmlString);
new bootstrap.Tooltip(hoveredGrid);
hoveredGrid.tooltip("show");
} else {
//disable the tooltip
hoveredGrid.attr("data-bs-title", "");
}
});
} else {
if (hoveredGrid.attr("data-bs-title") != '') {
hoveredGrid.tooltip("show");
}
if (hoveredGrid.attr("data-bs-title") != '') {
hoveredGrid.tooltip("show");
}
}
function hidePinnedNotes(vehicleId) {
@@ -249,4 +289,108 @@ function sortGarage(sender, isMobile) {
sortVehicles(false);
}
}
}
let dragged = null;
let draggedId = 0;
function dragEnter(event) {
event.preventDefault();
}
function dragStart(event, vehicleId) {
dragged = event.target;
draggedId = vehicleId;
event.dataTransfer.setData('text/plain', draggedId);
}
function dragOver(event) {
event.preventDefault();
}
function dropBox(event, targetVehicleId) {
if (dragged.parentElement != event.target && event.target != dragged && draggedId != targetVehicleId) {
copyContributors(draggedId, targetVehicleId);
}
event.preventDefault();
}
function copyContributors(sourceVehicleId, destVehicleId) {
var sourceVehicleName = $(`#gridVehicle_${sourceVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" ");
var destVehicleName = $(`#gridVehicle_${destVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" ");
Swal.fire({
title: "Copy Collaborators?",
text: `Copy collaborators over from ${sourceVehicleName} to ${destVehicleName}?`,
showCancelButton: true,
confirmButtonText: "Copy",
confirmButtonColor: "#0d6efd"
}).then((result) => {
if (result.isConfirmed) {
$.post('/Vehicle/DuplicateVehicleCollaborators', { sourceVehicleId: sourceVehicleId, destVehicleId: destVehicleId }, function (data) {
if (data.success) {
successToast("Collaborators Copied");
loadGarage();
} else {
errorToast(data.message);
}
})
} else {
$("#workAroundInput").hide();
}
});
}
function showAccountInformationModal() {
$.get('/Home/GetUserAccountInformationModal', function (data) {
$('#accountInformationModalContent').html(data);
$('#accountInformationModal').modal('show');
})
}
function hideAccountInformationModal() {
$('#accountInformationModal').modal('hide');
}
function validateAndSaveUserAccount() {
var hasError = false;
if ($('#inputUsername').val().trim() == '') {
$('#inputUsername').addClass("is-invalid");
hasError = true;
} else {
$('#inputUsername').removeClass("is-invalid");
}
if ($('#inputEmail').val().trim() == '') {
$('#inputEmail').addClass("is-invalid");
hasError = true;
} else {
$('#inputEmail').removeClass("is-invalid");
}
if ($('#inputToken').val().trim() == '') {
$('#inputToken').addClass("is-invalid");
hasError = true;
} else {
$('#inputToken').removeClass("is-invalid");
}
if (hasError) {
errorToast("Please check the form data");
return;
}
var userAccountInfo = {
userName: $('#inputUsername').val(),
password: $('#inputPassword').val(),
emailAddress: $('#inputEmail').val(),
token: $('#inputToken').val()
}
$.post('/Home/UpdateUserAccount', { userAccount: userAccountInfo }, function (data) {
if (data.success) {
//hide modal
hideAccountInformationModal();
successToast('Profile Updated');
performLogOut();
} else {
errorToast(data.message);
}
});
}
function generateTokenForUser() {
$.post('/Home/GenerateTokenForUser', function (data) {
if (data) {
successToast('Token sent');
} else {
errorToast(genericErrorMessage())
}
});
}

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