Compare commits

...

204 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
Hargata Softworks
92b9c6953c Merge pull request #283 from hargata/Hargata/calendar.view
fixed bug for records with deleted views.
2024-02-13 16:17:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ac1c2ca7fe fixed bug for records with deleted views. 2024-02-13 16:16:49 -07:00
Hargata Softworks
f05ca7f1f8 Merge pull request #282 from hargata/Hargata/calendar.view
Hargata/calendar.view
2024-02-13 16:03:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ecef1c8640 cleaner code. 2024-02-13 16:02:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
43d58a4b7e Fixed bug with styling. 2024-02-13 15:49:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8293b376a2 Added a Calendar View 2024-02-13 15:36:25 -07:00
Hargata Softworks
c243cdd54c Merge pull request #276 from hargata/Hargata/extra.fields
Extra Fields
2024-02-12 20:59:53 -07:00
Hargata Softworks
0383257356 Merge pull request #278 from hargata/Hargata/numeric.odo
Numeric keyboard for odometer field.
2024-02-12 20:56:33 -07:00
DESKTOP-GENO133\IvanPlex
73e8731bd4 Numeric keyboard for odometer field. 2024-02-12 20:56:03 -07:00
DESKTOP-GENO133\IvanPlex
b0e82889e0 updated translation, fixed postgres method for extra field insert. 2024-02-12 20:28:36 -07:00
Hargata Softworks
8e28116371 Merge pull request #277 from hargata/Hargata/documentation.static
Static File Documentation
2024-02-12 19:29:06 -07:00
DESKTOP-GENO133\IvanPlex
ed717af91c further optimization. 2024-02-12 19:26:11 -07:00
DESKTOP-GENO133\IvanPlex
7ae5a982e9 updated this thang. 2024-02-12 18:31:11 -07:00
DESKTOP-GENO133\IvanPlex
2e3f1e6763 updated translation file. 2024-02-12 18:30:44 -07:00
DESKTOP-GENO133\IvanPlex
4a2e02afc8 fixed bug where required fields don't persist. 2024-02-12 18:30:03 -07:00
DESKTOP-GENO133\IvanPlex
9c53850faa added front end for displaying extra fields. 2024-02-12 17:39:09 -07:00
DESKTOP-GENO133\IvanPlex
695b15faed added UI to add/edit/delete extra fields. 2024-02-12 17:36:13 -07:00
DESKTOP-GENO133\IvanPlex
33aeaf9825 front end logic for saving extra fields. 2024-02-12 17:32:08 -07:00
DESKTOP-GENO133\IvanPlex
dc3608524a fomatting. 2024-02-12 15:04:32 -07:00
DESKTOP-GENO133\IvanPlex
6996814888 added backend for storing extra fields. 2024-02-12 15:04:02 -07:00
DESKTOP-GENO133\IvanPlex
9bbaf49ade Static documentation cloned from otterwiki 2024-02-12 13:59:53 -07:00
DESKTOP-GENO133\IvanPlex
db737df712 removed empty extra fields from being added to object. 2024-02-12 09:15:05 -07:00
DESKTOP-GENO133\IvanPlex
bae1839c06 added JS to retrieve and validate extra fields. 2024-02-12 09:05:59 -07:00
DESKTOP-GENO133\IvanPlex
7e70f5108f changed dictionary to class object so we can enforce field requirement. 2024-02-12 03:27:26 -07:00
DESKTOP-GENO133\IvanPlex
c5c0a6900b added extra fields attribute to object classes. 2024-02-11 19:08:27 -07:00
Hargata Softworks
50b88dd7a1 Merge pull request #275 from hargata/Hargata/supplies.improvement
updated version number and fixed labels not updating in supplies page
2024-02-11 18:36:33 -07:00
DESKTOP-GENO133\IvanPlex
e44eb991f5 updated version number and fixed labels not updating in supplies page 2024-02-11 18:14:34 -07:00
Hargata Softworks
39d0d346ad Merge pull request #274 from hargata/Hargata/supplies.improvement
fixed supply requisition when used with european units.
2024-02-11 14:41:31 -07:00
DESKTOP-GENO133\IvanPlex
142865f1ce fixed supply requisition when used with european units. 2024-02-11 14:40:47 -07:00
Hargata Softworks
2c18755a0f Merge pull request #273 from hargata/Hargata/supplies.improvement
Supplies Improvement
2024-02-11 13:08:08 -07:00
DESKTOP-GENO133\IvanPlex
d3be77bf09 fixed div name, 2024-02-11 12:47:43 -07:00
DESKTOP-GENO133\IvanPlex
bcafd9e97e added tags in supplies requisition modal. 2024-02-11 12:46:34 -07:00
DESKTOP-GENO133\IvanPlex
f581e55842 added tags to general supply record tab. 2024-02-11 09:48:54 -07:00
DESKTOP-GENO133\IvanPlex
dfe4e4f1ab added check for missing supplies 2024-02-11 09:24:48 -07:00
Hargata Softworks
080e2a1667 Merge pull request #269 from hargata/Hargata/num.input
added attributes to prompt numeric inputs without actually limiting t…
2024-02-10 22:53:57 -07:00
DESKTOP-GENO133\IvanPlex
01cbee00fd added attributes to prompt numeric inputs without actually limiting the user to numbers. 2024-02-10 22:52:22 -07:00
143 changed files with 5242 additions and 737 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,20 +15,31 @@ 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;
public HomeController(ILogger<HomeController> logger,
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
ILoginLogic loginLogic,
IConfigHelper configuration,
IFileHelper fileHelper)
IFileHelper fileHelper,
IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper)
{
_logger = logger;
_dataAccess = dataAccess;
_config = configuration;
_userLogic = userLogic;
_fileHelper = fileHelper;
_extraFieldDataAccess = extraFieldDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
_loginLogic = loginLogic;
}
private int GetUserID()
{
@@ -47,6 +58,34 @@ namespace CarCareTracker.Controllers
}
return PartialView("_GarageDisplay", vehiclesStored);
}
public IActionResult Calendar()
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
List<ReminderRecordViewModel> reminders = new List<ReminderRecordViewModel>();
foreach (Vehicle vehicle in vehiclesStored)
{
var vehicleReminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
vehicleReminders.RemoveAll(x => x.Metric == ReminderMetric.Odometer);
//we don't care about mileages so we can basically fake the current vehicle mileage.
if (vehicleReminders.Any())
{
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, 0, DateTime.Now);
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
reminders.AddRange(reminderUrgency);
}
}
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);
@@ -61,14 +100,96 @@ 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);
if (recordExtraFields.Id != importMode)
{
recordExtraFields.Id = importMode;
}
return PartialView("_ExtraFields", recordExtraFields);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
public IActionResult UpdateExtraFields(RecordExtraField record)
{
try
{
var result = _extraFieldDataAccess.SaveExtraFields(record);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
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
{
@@ -52,7 +49,8 @@ namespace CarCareTracker.Controllers
"CREATE TABLE IF NOT EXISTS app.userrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)",
"CREATE TABLE IF NOT EXISTS app.tokenrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)",
"CREATE TABLE IF NOT EXISTS app.userconfigrecords (id INT primary key, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.useraccessrecords (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))"
"CREATE TABLE IF NOT EXISTS app.useraccessrecords (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))",
"CREATE TABLE IF NOT EXISTS app.extrafields (id INT primary key, data jsonb not null)"
};
foreach(string cmd in cmds)
{
@@ -97,6 +95,8 @@ namespace CarCareTracker.Controllers
var tokenrecords = new List<Token>();
var userconfigrecords = new List<UserConfigData>();
var useraccessrecords = new List<UserAccess>();
var extrafields = new List<RecordExtraField>();
#region "Part1"
string cmd = $"SELECT data FROM app.vehicles";
using (var ctext = pgDataSource.CreateCommand(cmd))
@@ -396,6 +396,25 @@ namespace CarCareTracker.Controllers
};
}
#endregion
#region "Part5"
cmd = $"SELECT data FROM app.extrafields";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
extrafields.Add(System.Text.Json.JsonSerializer.Deserialize<RecordExtraField>(reader["data"] as string));
}
}
foreach (var record in extrafields)
{
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<RecordExtraField>("extrafields");
table.Upsert(record);
};
}
#endregion
var destFilePath = $"{fullFolderPath}.zip";
ZipFile.CreateFromDirectory(fullFolderPath, destFilePath);
return Json(new OperationResponse { Success = true, Message = $"/{tempFolder}.zip" });
@@ -441,6 +460,8 @@ namespace CarCareTracker.Controllers
var tokenrecords = new List<Token>();
var userconfigrecords = new List<UserConfigData>();
var useraccessrecords = new List<UserAccess>();
var extrafields = new List<RecordExtraField>();
#region "Part1"
using (var db = new LiteDatabase(fullFileName))
{
@@ -704,6 +725,23 @@ namespace CarCareTracker.Controllers
}
}
#endregion
#region "Part5"
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<RecordExtraField>("extrafields");
extrafields = table.FindAll().ToList();
};
foreach (var record in extrafields)
{
string cmd = $"INSERT INTO app.extrafields (id, data) VALUES(@id, CAST(@data AS jsonb))";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("data", System.Text.Json.JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
#endregion
return Json(new OperationResponse { Success = true, Message = "Data Imported Successfully" });
}
catch (Exception ex)

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,30 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class ExtraFieldDataAccess: IExtraFieldDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "extrafields";
public RecordExtraField GetExtraFieldsById(int importMode)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<RecordExtraField>(tableName);
return table.FindById(importMode) ?? new RecordExtraField();
};
}
public bool SaveExtraFields(RecordExtraField record)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<RecordExtraField>(tableName);
table.Upsert(record);
return true;
};
}
}
}

View File

@@ -0,0 +1,76 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Npgsql;
using System.Text.Json;
namespace CarCareTracker.External.Implementations
{
public class PGExtraFieldDataAccess : IExtraFieldDataAccess
{
private NpgsqlDataSource pgDataSource;
private readonly ILogger<PGExtraFieldDataAccess> _logger;
private static string tableName = "extrafields";
public PGExtraFieldDataAccess(IConfiguration config, ILogger<PGExtraFieldDataAccess> logger)
{
pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]);
_logger = logger;
try
{
//create table if not exist.
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
public RecordExtraField GetExtraFieldsById(int importMode)
{
try
{
string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id";
var results = new RecordExtraField();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", importMode);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
RecordExtraField result = JsonSerializer.Deserialize<RecordExtraField>(reader["data"] as string);
results = result;
}
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new RecordExtraField();
}
}
public bool SaveExtraFields(RecordExtraField record)
{
try
{
var existingRecord = GetExtraFieldsById(record.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);
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
return ctext.ExecuteNonQuery() > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}

View File

@@ -0,0 +1,10 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IExtraFieldDataAccess
{
public RecordExtraField GetExtraFieldsById(int importMode);
public bool SaveExtraFields(RecordExtraField record);
}
}

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

@@ -8,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)
{
@@ -128,7 +130,9 @@ namespace CarCareTracker.Helper
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
Tags = input.Tags,
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
@@ -142,7 +146,9 @@ namespace CarCareTracker.Helper
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
Tags = input.Tags,
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
@@ -156,10 +162,45 @@ namespace CarCareTracker.Helper
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
Tags = input.Tags,
ExtraFields = input.ExtraFields,
RequisitionHistory = input.RequisitionHistory
};
}
public static List<ExtraField> AddExtraFields(List<ExtraField> recordExtraFields, List<ExtraField> templateExtraFields)
{
if (!templateExtraFields.Any()) {
return new List<ExtraField>();
}
if (!recordExtraFields.Any())
{
return templateExtraFields;
}
var fieldNames = templateExtraFields.Select(x => x.Name);
//remove fields that are no longer present in template.
recordExtraFields.RemoveAll(x => !fieldNames.Contains(x.Name));
if (!recordExtraFields.Any())
{
return templateExtraFields;
}
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;
}
public static string GetFuelEconomyUnit(bool useKwh, bool useHours, bool useMPG, bool useUKMPG)
{
string fuelEconomyUnit;
@@ -182,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; }
@@ -12,6 +13,21 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
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 }; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
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

@@ -19,5 +19,6 @@
public string Notes { get; set; }
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>();
}
}

View File

@@ -19,6 +19,7 @@
public string Notes { get; set; }
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 GasRecord ToGasRecord() { return new GasRecord {
Id = Id,
Cost = Cost,
@@ -30,7 +31,8 @@
IsFillToFull = IsFillToFull,
MissedFuelUp = MissedFuelUp,
Notes = Notes,
Tags = Tags
Tags = Tags,
ExtraFields = ExtraFields
}; }
}
}

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

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

View File

@@ -9,6 +9,7 @@
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>();
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files, Tags = Tags }; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
}
}

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; }
@@ -13,5 +14,7 @@
public PlanPriority Priority { get; set; }
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; }
@@ -14,9 +15,12 @@
public PlanPriority Priority { get; set; }
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,
@@ -25,7 +29,13 @@
ImportMode = ImportMode,
Cost = Cost,
Priority = Priority,
Progress = Progress
Progress = Progress,
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; }
@@ -12,6 +13,21 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
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 }; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
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

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class ExtraField
{
public string Name { get; set; }
public string Value { get; set; }
public bool IsRequired { get; set; }
}
}

View File

@@ -11,5 +11,7 @@
public string Notes { get; set; }
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,6 +35,7 @@
public string Description { get; set; }
public string Cost { get; set; }
public string Notes { get; set; }
public string Tags { get; set; }
}
public class ServiceRecordExportModel

View File

@@ -0,0 +1,14 @@
using LiteDB;
namespace CarCareTracker.Models
{
public class RecordExtraField
{
/// <summary>
/// Corresponds to int value of ImportMode enum
/// </summary>
[BsonId(false)]
public int Id { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
}
}

View File

@@ -33,5 +33,8 @@
/// </summary>
public string Notes { get; set; }
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

@@ -12,6 +12,9 @@
public decimal Cost { get; set; }
public string Notes { get; set; }
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,
@@ -22,6 +25,10 @@
Quantity = Quantity,
Description = Description,
Notes = Notes,
Files = Files }; }
Files = Files,
Tags = Tags,
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

@@ -13,5 +13,6 @@
public int CustomMonthInterval { get; set; } = 0;
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>();
}
}

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; }
@@ -13,6 +14,7 @@
public int CustomMonthInterval { get; set; } = 0;
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 TaxRecord ToTaxRecord() { return new TaxRecord {
Id = Id,
VehicleId = VehicleId,
@@ -24,7 +26,8 @@
RecurringInterval = RecurringInterval,
CustomMonthInterval = CustomMonthInterval,
Files = Files,
Tags = Tags
Tags = Tags,
ExtraFields = ExtraFields
}; }
}
}

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; }
@@ -12,6 +13,21 @@
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public List<string> Tags { get; set; } = new List<string>();
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 }; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
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();
@@ -31,6 +34,7 @@ if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){
builder.Services.AddSingleton<IUserRecordDataAccess, PGUserRecordDataAccess>();
builder.Services.AddSingleton<ITokenRecordDataAccess, PGTokenRecordDataAccess>();
builder.Services.AddSingleton<IUserAccessDataAccess, PGUserAccessDataAccess>();
builder.Services.AddSingleton<IExtraFieldDataAccess, PGExtraFieldDataAccess>();
}
else
{
@@ -50,6 +54,7 @@ else
builder.Services.AddSingleton<IUserRecordDataAccess, UserRecordDataAccess>();
builder.Services.AddSingleton<ITokenRecordDataAccess, TokenRecordDataAccess>();
builder.Services.AddSingleton<IUserAccessDataAccess, UserAccessDataAccess>();
builder.Services.AddSingleton<IExtraFieldDataAccess, ExtraFieldDataAccess>();
}
//configure helpers

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,9 +24,12 @@
@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")</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>
</li>
@@ -38,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>
@@ -64,6 +73,9 @@
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</button>
</li>
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</button>
</li>
@@ -78,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>
@@ -93,6 +111,8 @@
</div>
<div class="tab-pane fade" id="supply-tab-pane" role="tabpanel" tabindex="0">
</div>
<div class="tab-pane fade" id="calendar-tab-pane" role="tabpanel" tabindex="0">
</div>
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
</div>
</div>
@@ -110,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

@@ -0,0 +1,25 @@
@using CarCareTracker.Helper
@model List<ReminderRecordViewModel>
<script>
var eventDates = [];
var groupedDates = [];
@foreach(ReminderRecordViewModel reminderRecord in Model)
{
@: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">
<div class="col-12 reminderCalendarView">
<div class="reminderCalendarViewContent" style="height:60vh;">
</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

@@ -0,0 +1,125 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model RecordExtraField
<script>
var extraFields = [];
</script>
<div class="modal-header">
<h5 class="modal-title">@(translator.Translate(userLanguage, "Manage Extra Fields"))</h5>
<button type="button" class="btn-close" onclick="hideExtraFieldModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-12">
<select class="form-select" onchange="refreshExtraFieldModal()" id="extraFieldTab">
<!option @(Model.Id == (int)ImportMode.ServiceRecord ? "selected" : "") value="@((int)ImportMode.ServiceRecord)">@translator.Translate(userLanguage, "Service Record")</!option>
<!option @(Model.Id == (int)ImportMode.RepairRecord ? "selected" : "") value="@((int)ImportMode.RepairRecord)">@translator.Translate(userLanguage, "Repairs")</!option>
<!option @(Model.Id == (int)ImportMode.UpgradeRecord ? "selected" : "") value="@((int)ImportMode.UpgradeRecord)">@translator.Translate(userLanguage, "Upgrades")</!option>
<!option @(Model.Id == (int)ImportMode.GasRecord ? "selected" : "") value="@((int)ImportMode.GasRecord)">@translator.Translate(userLanguage, "Fuel")</!option>
<!option @(Model.Id == (int)ImportMode.TaxRecord ? "selected" : "") value="@((int)ImportMode.TaxRecord)">@translator.Translate(userLanguage, "Tax")</!option>
<!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>
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-8">@translator.Translate(userLanguage, "Name")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Required")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody>
@if (Model.ExtraFields != null)
{
@foreach (ExtraField extraField in Model.ExtraFields)
{
<script>
extraFields.push({ name: decodeHTMLEntities('@extraField.Name'), isRequired: @extraField.IsRequired.ToString().ToLower()});
</script>
<tr class="d-flex">
<td class="col-8">@extraField.Name</td>
<td class="col-2"><input class="form-check-input" type="checkbox" onchange="updateExtraFieldIsRequired(decodeHTMLEntities('@extraField.Name'), this)" value="" @(extraField.IsRequired ? "checked" : "") /></td>
<td class="col-2"><button type="button" onclick="deleteExtraField(decodeHTMLEntities('@extraField.Name'))" class="btn btn-danger"><i class="bi bi-trash"></i></button></td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideExtraFieldModal()">@translator.Translate(userLanguage, "Close")</button>
<button type="button" class="btn btn-primary" onclick="addExtraField()">@translator.Translate(userLanguage, "Add New Field")</button>
</div>
<script>
var importMode = @Model.Id;
function addExtraField() {
Swal.fire({
title: 'Field Name',
html: `
<input type="text" id="inputExtraFieldName" class="swal2-input" placeholder="Field Name">
`,
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const extraFieldName = $("#inputExtraFieldName").val();
if (!extraFieldName || extraFieldName.trim() == '') {
Swal.showValidationMessage(`Field names cannot be blank`);
} else if (extraFields.filter(x => x.name == extraFieldName).length > 0) {
Swal.showValidationMessage(`Field names must be unique`);
}
return { extraFieldName }
},
}).then(function (result) {
if (result.isConfirmed) {
extraFields.push({ name: result.value.extraFieldName, isRequired: false});
updateExtraFields();
}
});
}
function updateExtraFieldIsRequired(fieldId, checkbox){
var indexToEdit = extraFields.findIndex(x => x.name == fieldId);
var extraFieldToEdit = extraFields[indexToEdit];
extraFieldToEdit.isRequired = $(checkbox).is(":checked");
updateExtraFields();
}
function deleteExtraField(fieldId) {
extraFields = extraFields.filter(x => x.name != fieldId);
updateExtraFields();
}
function refreshExtraFieldModal() {
var selectedExtraFieldTab = $("#extraFieldTab").val();
$.post(`/Home/GetExtraFieldsModal?importMode=${selectedExtraFieldTab}`, function (data) {
if (data) {
$("#extraFieldModalContent").html(data);
} else {
errorToast(genericErrorMessage());
}
});
}
function updateExtraFields() {
$.post('/Home/UpdateExtraFields', { id: importMode, extraFields: extraFields }, function (data) {
if (data) {
$("#extraFieldModalContent").html(data);
} else {
errorToast(genericErrorMessage());
}
});
}
</script>

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>
@@ -178,6 +186,19 @@
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Server-wide Settings")</span>
<div class="row">
<div class="col-6 d-grid">
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Extra Fields")</button>
</div>
<div class="col-6 d-grid">
<button onclick="showReminderUrgencyThresholdModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Reminders")</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
@@ -191,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.5</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.
@@ -229,7 +250,69 @@
</ul>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="extraFieldModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="extraFieldModalContent">
</div>
</div>
</div>
<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);
$("#extraFieldModal").modal('show');
});
}
function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide');
}
function getCheckedTabs() {
var visibleTabs = $("#visibleTabs :checked").map(function () {
return this.value;
@@ -262,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(),
@@ -283,13 +368,13 @@
window.location.href = data;
});
}
function openUploadLanguage(){
function openUploadLanguage() {
$("#inputLanguage").click();
}
function openRestoreBackup() {
$("#inputBackup").click();
}
function uploadLanguage(event){
function uploadLanguage(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
@@ -308,7 +393,7 @@
errorToast(response.message);
}
},
error: function(){
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
}
@@ -351,9 +436,9 @@
Swal.fire({
title: 'Setup Credentials',
html: `
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
`,
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
`,
confirmButtonText: 'Setup',
focusConfirm: false,
preConfirm: () => {

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

@@ -23,11 +23,19 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="collisionRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
<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" id="collisionRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the repair")" value="@(isNew ? "" : Model.Cost)">
<input type="text" inputmode="decimal" id="collisionRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the repair")" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
{
@await Html.PartialAsync("_SupplyStore", "RepairRecord")
@@ -39,6 +47,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>
}
</div>
<div class="col-md-6 col-12">
<label for="collisionRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -48,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>
}
@@ -64,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>
@@ -75,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">
@@ -97,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

@@ -52,9 +52,9 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="gasRecordMileage">@($"{translator.Translate(userLanguage,"Odometer Reading")}({distanceUnit})")</label>
<input type="number" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<label for="gasRecordGallons">@($"{translator.Translate(userLanguage, "Fuel Consumption")}({consumptionUnit})")</label>
<input type="text" id="gasRecordGallons" class="form-control" placeholder="@translator.Translate(userLanguage,"Amount of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Gallons)">
<input type="text" inputmode="decimal" id="gasRecordGallons" class="form-control" placeholder="@translator.Translate(userLanguage,"Amount of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Gallons)">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="gasIsFillToFull" checked="@Model.GasRecord.IsFillToFull">
<label class="form-check-label" for="gasIsFillToFull">@translator.Translate(userLanguage,"Is Filled To Full")</label>
@@ -67,7 +67,7 @@
@if (isNew)
{
<div class="input-group">
<input type="text" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Cost)">
<input type="text" inputmode="decimal" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Cost)">
<div class="input-group-text">
<select class="form-select form-select-sm" id="gasCostType">
<option value="total">@translator.Translate(userLanguage,"Total")</option>
@@ -77,7 +77,7 @@
</div>
} else
{
<input type="text" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Cost)">
<input type="text" inputmode="decimal" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Cost)">
}
<label for="gasRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="gasRecordTag">
@@ -86,6 +86,14 @@
<!option value="@tag">@tag</!option>
}
</select>
@foreach (ExtraField field in Model.GasRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
</div>
<div class="col-md-6 col-12">
<label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -95,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

@@ -23,7 +23,7 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="odometerRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading")" value="@(isNew ? "" : Model.Mileage)">
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading")" value="@(isNew ? "" : Model.Mileage)">
<label for="odometerRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="odometerRecordTag">
@foreach (string tag in Model.Tags)
@@ -31,6 +31,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>
}
</div>
<div class="col-md-6 col-12">
<label for="odometerRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -40,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

@@ -20,7 +20,7 @@
<label for="planRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="planRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage, "Describe the Plan")" value="@Model.Description">
<label for="planRecordCost">@translator.Translate(userLanguage, "Cost")</label>
<input type="text" id="planRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage, "Cost of the Plan")" value="@Model.Cost">
<input type="text" inputmode="decimal" id="planRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage, "Cost of the Plan")" value="@Model.Cost">
@if (isNew)
{
@await Html.PartialAsync("_SupplyStore", "PlanRecord")
@@ -43,6 +43,14 @@
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select>
@foreach (ExtraField field in Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
@if (!isNew)
{
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>
@@ -57,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>
@@ -77,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>
@@ -89,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>
}
@@ -98,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 = [];
@@ -111,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

@@ -33,9 +33,9 @@
<label class="form-check-label" for="reminderMetricOdometer">@translator.Translate(userLanguage,"Odometer")</label>
</div>
<div class="input-group">
<input type="number" id="reminderMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Future Odometer Reading")" value="@(isNew ? "" : Model.Mileage)">
<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

@@ -23,11 +23,19 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="serviceRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew ? "" : Model.Mileage)">
<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" id="serviceRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the service")" value="@(isNew ? "" : Model.Cost)">
<input type="text" inputmode="decimal" id="serviceRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the service")" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
{
@await Html.PartialAsync("_SupplyStore", "ServiceRecord")
@@ -39,6 +47,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>
}
</div>
<div class="col-md-6 col-12">
<label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -48,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>
}
@@ -64,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>
@@ -75,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">
@@ -97,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,13 +31,33 @@
<div class="row">
<div class="col-md-6 col-12">
<label for="supplyRecordQuantity">@translator.Translate(userLanguage,"Quantity")</label>
<input type="text" 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>
<input type="text" id="supplyRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost")" value="@(isNew ? "" : Model.Cost)">
<input type="text" inputmode="decimal" id="supplyRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost")" value="@(isNew ? "" : Model.Cost)">
</div>
</div>
<label for="supplyRecordTag">@translator.Translate(userLanguage, "Tags(optional)")</label>
<select multiple class="form-select" id="supplyRecordTag">
@foreach (string tag in Model.Tags)
{
<!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>
}
</div>
<div class="col-md-6 col-12">
<label for="supplyRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -47,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>
@@ -65,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>
@@ -77,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

@@ -6,13 +6,30 @@
var userLanguage = userConfig.UserLanguage;
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">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage,"# of Supply Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"{translator.Translate(userLanguage,"Total")}: {Model.Sum(x => x.Cost).ToString("C")}")</span>
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage, "# of Supply 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('supply-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
}
<datalist id="tagList">
@foreach (string recordTag in recordTags)
{
<!option value="@recordTag"></!option>
}
</datalist>
</div>
<div>
@if (enableCsvImports)
@@ -27,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>
}
@@ -47,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)">
<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">@((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>
@@ -80,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

@@ -4,6 +4,7 @@
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
}
@model List<SupplyRecord>
<div class="modal-header">
@@ -14,10 +15,16 @@
@if (Model.Any())
{
<div class="row">
<div class="col-12" style="max-height:50vh; overflow-y:auto;">
<div class="col-12" style="max-height:50vh; overflow-y:auto;" id="supplies-table">
<div class="alert alert-warning" role="alert">
@translator.Translate(userLanguage,"Supplies are requisitioned immediately after the record is created and cannot be modified. If you have incorrectly entered the amount you needed you will need to correct it in the Supplies tab.")
</div>
<div class="d-flex align-items-center flex-wrap">
@foreach (string recordTag in recordTags)
{
<span onclick="filterTable('supplies-table', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
}
</div>
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
@@ -32,9 +39,9 @@
<tbody>
@foreach (SupplyRecord supplyRecord in Model)
{
<tr class="d-flex" id="supplyRows">
<tr class="d-flex" id="supplyRows" data-tags='@string.Join(" ", supplyRecord.Tags)'>
<td class="col-1"><input class="form-check-input" type="checkbox" onchange="toggleQuantityFieldDisabled(this)" value="@supplyRecord.Id"></td>
<td class="col-2"><input type="text" disabled onchange="recalculateTotal()" class="form-control"></td>
<td class="col-2"><input type="text" inputmode="decimal" disabled onchange="recalculateTotal()" class="form-control"></td>
<td class="col-2 supplyquantity">@supplyRecord.Quantity</td>
<td class="col-2 text-truncate">@StaticHelper.TruncateStrings(supplyRecord.PartNumber)</td>
<td class="col-3 text-truncate">@StaticHelper.TruncateStrings(supplyRecord.Description)</td>
@@ -122,7 +129,7 @@
$("#selectSuppliesButton").attr('disabled', (hasError || totalSum == 0));
if (!hasError) {
return {
totalSum: totalSum,
totalSum: globalFloatToString(totalSum),
selectedSupplies: selectedSupplies.toArray()
};
} else {

View File

@@ -24,8 +24,16 @@
</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" id="taxRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of tax paid")" value="@(isNew? "" : Model.Cost)">
<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>
<select multiple class="form-select" id="taxRecordTag">
@foreach (string tag in Model.Tags)
@@ -33,6 +41,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>
}
</div>
<div class="col-md-6 col-12">
<label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -57,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>
}
@@ -73,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>
@@ -99,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

@@ -23,11 +23,19 @@
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="upgradeRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew ? "" : Model.Mileage)">
<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" id="upgradeRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the upgrade/mods")" value="@(isNew ? "" : Model.Cost)">
<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)
{
@await Html.PartialAsync("_SupplyStore", "UpgradeRecord")
@@ -39,6 +47,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>
}
</div>
<div class="col-md-6 col-12">
<label for="upgradeRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
@@ -48,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>
}
@@ -64,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>
@@ -75,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">
@@ -97,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

@@ -27,13 +27,17 @@
<form class="form-inline">
<div class="form-group">
<label for="inputYear">@translator.Translate(userLanguage, "Year")</label>
<input type="number" id="inputYear" class="form-control" placeholder="@translator.Translate(userLanguage, "Year(must be after 1900)")" value="@(isNew ? "" : Model.Year)">
<input type="number" inputmode="numeric" id="inputYear" class="form-control" placeholder="@translator.Translate(userLanguage, "Year(must be after 1900)")" value="@(isNew ? "" : Model.Year)">
<label for="inputMake">@translator.Translate(userLanguage, "Make")</label>
<input type="text" id="inputMake" class="form-control" placeholder="@translator.Translate(userLanguage, "Make")" value="@Model.Make">
<label for="inputModel">@translator.Translate(userLanguage, "Model")</label>
<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>

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