Compare commits

...

193 Commits

Author SHA1 Message Date
Hargata Softworks
27126638be Merge pull request #244 from hargata/Hargata/deltamileage
swap min max label on filter.
2024-02-07 13:12:38 -07:00
DESKTOP-GENO133\IvanPlex
8c3a001930 swap min max label on filter. 2024-02-07 13:11:18 -07:00
Hargata Softworks
2b2f181888 Merge pull request #243 from hargata/Hargata/deltamileage
fixed max and min labels not updating.
2024-02-07 12:49:04 -07:00
DESKTOP-GENO133\IvanPlex
66557fa126 fixed max and min labels not updating. 2024-02-07 12:48:34 -07:00
Hargata Softworks
7c00951d74 Merge pull request #242 from hargata/Hargata/deltamileage
Hargata/deltamileage
2024-02-07 12:32:36 -07:00
DESKTOP-GENO133\IvanPlex
1087ed56ce added feature to auto add to odometer when user adds via API. 2024-02-07 10:51:04 -07:00
DESKTOP-GENO133\IvanPlex
dd2cfd90b1 added method to toggle delta mileage. 2024-02-07 10:41:58 -07:00
Hargata Softworks
cc43d45c9c Merge pull request #234 from hargata/Hargata/email.reminder.template
created email template for reminder emails.
2024-02-06 16:13:04 -07:00
Hargata Softworks
dba51f171d Merge pull request #237 from hargata/Hargata/plan.template
Planner Templates
2024-02-06 16:07:26 -07:00
DESKTOP-GENO133\IvanPlex
0bbf9f783f added more translation keys. 2024-02-06 16:04:34 -07:00
DESKTOP-GENO133\IvanPlex
a52ac6a41b Front end for creating plan records from templates. 2024-02-06 15:54:32 -07:00
DESKTOP-GENO133\IvanPlex
a542c91dc3 added backend for saving and retrieving plan record templates. 2024-02-06 12:45:39 -07:00
DESKTOP-GENO133\IvanPlex
6bcbb0cc86 created email template for reminder emails. 2024-02-06 06:12:13 -07:00
DESKTOP-GENO133\IvanPlex
7a78d51c79 Update version number. 2024-02-05 20:30:13 -07:00
Hargata Softworks
3c2715840e Merge pull request #233 from hargata/Hargata/increase.upload.size
1.1.3
2024-02-05 18:29:36 -07:00
DESKTOP-GENO133\IvanPlex
dfbd90aaf4 Merge branch 'main' into Hargata/increase.upload.size
# Conflicts:
#	Views/Login/Index.cshtml
2024-02-05 18:26:14 -07:00
DESKTOP-GENO133\IvanPlex
2b3a4ce8ce more things to translate 2024-02-05 18:23:06 -07:00
DESKTOP-GENO133\IvanPlex
135292f2b4 added attachment features to Notes. 2024-02-05 18:08:54 -07:00
Hargata Softworks
499c697b0b Merge pull request #229 from hargata/Hargata/redirectURL
added redirectURL
2024-02-05 08:46:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
fb74f01609 added redirectURL 2024-02-05 08:44:49 -07:00
DESKTOP-GENO133\IvanPlex
6ab2216742 greatly increase the max upload size. 2024-02-04 15:32:34 -07:00
Hargata Softworks
7c97aa70a0 Merge pull request #228 from hargata/Hargata/translation.helper
added try catch to translation layer for translation files error.
2024-02-04 12:56:51 -07:00
DESKTOP-GENO133\IvanPlex
ff255e8293 added try catch to translation layer for translation files error. 2024-02-04 12:56:24 -07:00
Hargata Softworks
57dc976a9f Merge pull request #227 from jdh313/docker-image-tags
ci: generate versioned tags
2024-02-04 11:46:27 -07:00
Jacob Hoehler
13361241e8 ci: generate versioned tags
Generates an image tag based on release tag, as well as generating a `latest` tag. Also generates an `edge` tag for the latest pushes to the main branch.
2024-02-04 13:17:45 -05:00
Hargata Softworks
332af9a04a Merge pull request #224 from hargata/Hargata/translation.helper
UI Translation
2024-02-04 10:55:09 -07:00
DESKTOP-GENO133\IvanPlex
961c60f936 fixed menu styling. 2024-02-04 10:53:23 -07:00
DESKTOP-GENO133\IvanPlex
34483886be added translation volume so that it can persist. 2024-02-04 10:49:22 -07:00
DESKTOP-GENO133\IvanPlex
548989f9ae keyed admin panel 2024-02-04 10:09:57 -07:00
DESKTOP-GENO133\IvanPlex
83a59aff37 set different path for default language. 2024-02-03 22:49:18 -07:00
DESKTOP-GENO133\IvanPlex
60f28b16e7 Merge remote-tracking branch 'origin/main' into Hargata/translation.helper 2024-02-03 22:30:52 -07:00
DESKTOP-GENO133\IvanPlex
9cb8bbd5b7 added default language settings. 2024-02-03 22:15:53 -07:00
DESKTOP-GENO133\IvanPlex
f7c95e9b36 more forgotten ones. 2024-02-03 22:11:47 -07:00
DESKTOP-GENO133\IvanPlex
cd37eedd15 updated default translation file. 2024-02-03 22:10:42 -07:00
DESKTOP-GENO133\IvanPlex
7d11c4003f keyed the rest. 2024-02-03 21:58:17 -07:00
DESKTOP-GENO133\IvanPlex
7d9b67a04d keyed taxes 2024-02-03 20:51:18 -07:00
DESKTOP-GENO133\IvanPlex
aba178a324 fix html encodings. 2024-02-03 20:36:32 -07:00
Hargata Softworks
4ecdaf5225 Merge pull request #225 from hargata/Hargata/unified.error.message
Fixed Odometer Validation
2024-02-03 20:16:39 -07:00
DESKTOP-GENO133\IvanPlex
c0bc9f7a14 fix odometer validation. 2024-02-03 20:13:59 -07:00
DESKTOP-GENO133\IvanPlex
094d96d880 unified error messages. 2024-02-03 20:07:05 -07:00
DESKTOP-GENO133\IvanPlex
a8fad6f160 keyed supplies 2024-02-03 18:46:21 -07:00
DESKTOP-GENO133\IvanPlex
6befd5ca76 keyed service record. 2024-02-03 17:51:26 -07:00
DESKTOP-GENO133\IvanPlex
046812ff27 keyed reminders and report page. 2024-02-03 16:51:07 -07:00
DESKTOP-GENO133\IvanPlex
db1778a749 keyed plans 2024-02-03 15:38:57 -07:00
DESKTOP-GENO133\IvanPlex
f27fa1625c missed a few tags. 2024-02-03 15:13:59 -07:00
DESKTOP-GENO133\IvanPlex
39a9d5f95b keyed up to odometer. 2024-02-03 14:41:33 -07:00
DESKTOP-GENO133\IvanPlex
234d7163fc added caching to reduce number of IO reads. 2024-02-03 11:01:54 -07:00
DESKTOP-GENO133\IvanPlex
6f64cc9996 keyed labels. 2024-02-03 10:10:15 -07:00
DESKTOP-GENO133\IvanPlex
58e3b49bdf updated en_US translation. 2024-02-03 10:04:47 -07:00
DESKTOP-GENO133\IvanPlex
ca2dc1e1e0 keyed gas modal. 2024-02-03 10:03:38 -07:00
DESKTOP-GENO133\IvanPlex
c65dca4c58 removed hardcoded string from gasrecord js. 2024-02-03 09:57:28 -07:00
DESKTOP-GENO133\IvanPlex
82c2351b00 keyed gas tab. 2024-02-03 09:51:42 -07:00
DESKTOP-GENO133\IvanPlex
f31ca163e5 Merge branch 'Hargata/tags.export' into Hargata/translation.helper 2024-02-03 06:28:32 -07:00
Hargata Softworks
351db5de95 Merge pull request #222 from hargata/Hargata/tags.export
added tags export to csv.
2024-02-03 06:13:06 -07:00
DESKTOP-GENO133\IvanPlex
41164516f7 added tags export to csv. 2024-02-03 06:11:59 -07:00
DESKTOP-GENO133\IvanPlex
29a3c815fc added translation helper. 2024-02-02 21:29:44 -07:00
Hargata Softworks
1768063e57 Merge pull request #218 from hargata/Hargata/update.count
Update count and aggregates after filtering by tags.
2024-02-02 17:05:02 -07:00
Hargata Softworks
35ce5603e5 Merge pull request #219 from hargata/Hargata/gas.tags
Hargata/gas.tags
2024-02-02 17:04:28 -07:00
DESKTOP-GENO133\IvanPlex
19e38f9885 fixed gas odometer reading validation. 2024-02-02 17:03:56 -07:00
DESKTOP-GENO133\IvanPlex
c55528b8a0 Re-calculate gas tag aggregate labels. 2024-02-02 16:58:37 -07:00
DESKTOP-GENO133\IvanPlex
1c00f31312 added tags functionality to gas tab. 2024-02-02 15:33:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8e66530544 Update count and aggregates after filtering by tags. 2024-02-02 12:15:34 -07:00
Hargata Softworks
7bdd3d902b Merge pull request #217 from hargata/Hargata/tags.other.tab
add tagging functionality to notes, odometer, and tax tabs
2024-02-02 09:08:17 -07:00
DESKTOP-T0O5CDB\DESK-555BD
abb44608fe add tagging functionality to notes, odometer, and tax tabs 2024-02-02 09:06:11 -07:00
Hargata Softworks
0b1e3f4be8 Merge pull request #216 from tylerbrewer2/main
merge GHA build and push workflows
2024-02-02 08:11:19 -07:00
tylerbrewer2
156eb8ec27 merge GHA build and push workflows 2024-02-02 10:07:13 -05:00
Hargata Softworks
472dcac590 Merge pull request #215 from hargata/Hargata/sort.gas.tab
Hargata/sort.gas.tab
2024-02-02 07:09:03 -07:00
DESKTOP-GENO133\IvanPlex
b4d86f415c only sort if garage is active tab 2024-02-02 07:07:04 -07:00
DESKTOP-GENO133\IvanPlex
812e768f93 added sorting functionality to touch screen devices. 2024-02-02 07:01:42 -07:00
Hargata Softworks
a08d4798e1 Merge pull request #214 from hargata/Hargata/sort.gas.tab
Hargata/sort.gas.tab
2024-02-01 20:34:32 -07:00
DESKTOP-GENO133\IvanPlex
7bc08c9950 added stripe link. 2024-02-01 20:33:44 -07:00
DESKTOP-GENO133\IvanPlex
20107e3f05 Merge branch 'main' into Hargata/sort.gas.tab 2024-02-01 20:22:16 -07:00
DESKTOP-GENO133\IvanPlex
ed678c36a5 add sorting to garage tab. 2024-02-01 20:21:49 -07:00
Hargata Softworks
f7574a9a60 Merge pull request #212 from hargata/Hargata/tags.autocomplete
added datalist to tags field.
2024-02-01 16:59:56 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e506b543e6 added datalist to tags field. 2024-02-01 13:50:30 -07:00
Hargata Softworks
019a549b64 Merge pull request #211 from hargata/Hargata/fixed.gas.fr
fixed gas settings for real
2024-02-01 10:16:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8dbc883a8c fixed gas settings for real 2024-02-01 10:14:46 -07:00
Hargata Softworks
5fd918115c Merge pull request #210 from hargata/Hargata/gas.settings.fix
prevent the settings page from overriding the fuel tab settings.
2024-02-01 09:34:05 -07:00
DESKTOP-GENO133\IvanPlex
a11f8377fe prevent the settings page from overriding the fuel tab settings. 2024-02-01 09:32:47 -07:00
Hargata Softworks
7be19f8603 Merge pull request #209 from hargata/Hargata/utf.bug.dashboard
Fix UTF encoding bug in dashboard.
2024-02-01 07:00:41 -07:00
DESKTOP-GENO133\IvanPlex
5407ebecb1 Fix UTF encoding bug in dashboard. 2024-02-01 06:59:54 -07:00
Hargata Softworks
b75a1b4e6c Update README.md 2024-01-31 17:56:17 -07:00
Hargata Softworks
1c28039aaf Merge pull request #207 from hargata/Hargata/persist.fuel.settings
fix user preferrred unit in vehicle consolidated report.
2024-01-31 17:44:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3a1836ef84 fix user preferrred unit in vehicle consolidated report. 2024-01-31 17:43:38 -07:00
Hargata Softworks
c747889f85 Merge pull request #206 from hargata/Hargata/persist.fuel.settings
persist gas tab settings.
2024-01-31 17:28:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
338a8426b2 persist gas tab settings. 2024-01-31 17:14:26 -07:00
Hargata Softworks
4b1701d158 Merge pull request #204 from hargata/Hargata/british.fuelunit.toggle
take into account unicode encoding.
2024-01-31 15:13:59 -07:00
DESKTOP-GENO133\IvanPlex
5d64a9a422 take into account unicode encoding. 2024-01-31 15:13:14 -07:00
Hargata Softworks
c2315db127 Merge pull request #203 from hargata/Hargata/british.fuelunit.toggle
Added alternate fuel units.
2024-01-31 14:30:52 -07:00
DESKTOP-GENO133\IvanPlex
e40129f701 Added alternate fuel units. 2024-01-31 14:29:35 -07:00
Hargata Softworks
d198ad9a6b Merge pull request #202 from hargata/Hargata/deltamileage.zero.fix
prevent delta mileage from being 0.
2024-01-31 11:39:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
abffc5ab60 prevent delta mileage from being 0. 2024-01-31 11:39:14 -07:00
Hargata Softworks
24ff9db32d Merge pull request #200 from hargata/Hargata/gas.tweaks
adjust for missed fuel ups.
2024-01-31 11:05:57 -07:00
DESKTOP-GENO133\IvanPlex
07fa560344 adjust for missed fuel ups. 2024-01-31 11:02:40 -07:00
Hargata Softworks
28615d9038 Merge pull request #198 from hargata/Hargata/tags
Hargata/tags
2024-01-31 09:14:55 -07:00
DESKTOP-GENO133\IvanPlex
d8f53e4b65 removed comment, 2024-01-31 09:13:13 -07:00
DESKTOP-GENO133\IvanPlex
1f0cef4161 Merge branch 'Hargata/taghelper.fix' into Hargata/tags
# Conflicts:
#	Views/Shared/_Layout.cshtml
2024-01-31 09:07:20 -07:00
Hargata Softworks
af5d72e0d0 Merge pull request #197 from hargata/Hargata/taghelper.fix
removed taghelper methods.
2024-01-31 09:03:57 -07:00
DESKTOP-GENO133\IvanPlex
1397e92709 removed taghelper methods. 2024-01-31 09:03:10 -07:00
DESKTOP-GENO133\IvanPlex
a49121b1d9 fixed tagsinput and tooltip js error. 2024-01-31 08:44:59 -07:00
DESKTOP-GENO133\IvanPlex
ca65a6fb71 added spacebar as an action key. 2024-01-31 08:22:01 -07:00
DESKTOP-GENO133\IvanPlex
a54857c6bf added vehicle tags. 2024-01-31 08:10:49 -07:00
DESKTOP-GENO133\IvanPlex
2eadd05289 added tagging functionality to upgrades and repairs. 2024-01-30 22:44:25 -07:00
DESKTOP-GENO133\IvanPlex
6eafa5e036 fixed hiding styles. 2024-01-30 21:26:21 -07:00
DESKTOP-GENO133\IvanPlex
40f01ad100 tags filter. 2024-01-30 20:23:08 -07:00
DESKTOP-GENO133\IvanPlex
161b36325e modified tagsinput to use modern bootstrap styles, added tagging functionality to service record. 2024-01-30 18:59:35 -07:00
DESKTOP-GENO133\IvanPlex
f9a6ddd513 add tags 2024-01-30 18:06:39 -07:00
Hargata Softworks
e85110f8b6 Merge pull request #193 from hargata/Hargata/custom.mileage.interval
Custom Mileage Interval for Reminders
2024-01-30 17:19:39 -07:00
DESKTOP-GENO133\IvanPlex
76d3dba574 fix label display 2024-01-30 17:18:20 -07:00
DESKTOP-GENO133\IvanPlex
178b50a033 added ability for user to define custom mileage intervals. 2024-01-30 17:15:21 -07:00
Hargata Softworks
fb45aefde3 Merge pull request #192 from hargata/Hargata/alternate.logo
replace logo in login pages.
2024-01-30 15:32:57 -07:00
DESKTOP-GENO133\IvanPlex
05bbc8a95b replace logo in login pages. 2024-01-30 15:32:09 -07:00
Hargata Softworks
f123299dd6 Merge pull request #191 from hargata/Hargata/alternate.logo
prevent pinned note from being loaded over and over again.
2024-01-30 14:54:02 -07:00
DESKTOP-GENO133\IvanPlex
7174413e2a prevent pinned note from being loaded over and over again. 2024-01-30 14:53:10 -07:00
Hargata Softworks
d816f73598 Merge pull request #190 from hargata/Hargata/alternate.logo
allows users to inject their own logo.
2024-01-30 14:43:51 -07:00
DESKTOP-GENO133\IvanPlex
b249ccdd6c allows users to inject their own logo. 2024-01-30 14:42:54 -07:00
Hargata Softworks
4a007530a6 Merge pull request #188 from hargata/Hargata/parse.url
added setting to automatically load parsed markdown.
2024-01-30 08:19:04 -07:00
DESKTOP-GENO133\IvanPlex
04ce448b23 removed onclick event since it ain;t helping. 2024-01-30 08:18:45 -07:00
DESKTOP-GENO133\IvanPlex
ccd446f299 added setting to automatically load parsed markdown. 2024-01-30 08:16:28 -07:00
Hargata Softworks
c49c8a5301 Merge pull request #186 from hargata/Hargata/parse.url
Hargata/parse.url
2024-01-29 21:05:24 -07:00
DESKTOP-GENO133\IvanPlex
55cc2819d0 show overlay in place instead. 2024-01-29 21:04:22 -07:00
DESKTOP-GENO133\IvanPlex
f8de7de0d6 added markdown for named notes. 2024-01-29 20:33:37 -07:00
Hargata Softworks
2ea1bc2c20 Merge pull request #185 from hargata/Hargata/parse.url
Added Markdown Parsing.
2024-01-29 19:53:38 -07:00
DESKTOP-GENO133\IvanPlex
90d095ea51 Updated version. 2024-01-29 19:50:01 -07:00
DESKTOP-GENO133\IvanPlex
e1d12d0918 added dependency 2024-01-29 19:47:45 -07:00
DESKTOP-GENO133\IvanPlex
d9d0957040 added method to parse markdown data. 2024-01-29 19:43:53 -07:00
Hargata Softworks
9d73db3c51 Merge pull request #184 from hargata/Hargata/reduce.login.error
added env variable to reduce auth-related logs.
2024-01-29 17:21:07 -07:00
DESKTOP-GENO133\IvanPlex
c0f0786fd4 added env variable to reduce auth-related logs. 2024-01-29 17:20:39 -07:00
Hargata Softworks
357eff116f Merge pull request #183 from hargata/Hargata/export.attachments
Export attachments for vehicle.
2024-01-29 13:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
32a047c522 added print function. 2024-01-29 13:32:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8a237bb7ec Export attachments for vehicle. 2024-01-29 13:24:30 -07:00
DESKTOP-GENO133\IvanPlex
f5b9072cc6 Updated Github pages. 2024-01-28 15:06:06 -07:00
Hargata Softworks
4e3eaa53ff Merge pull request #180 from hargata/Hargata/hr-mpg
Hargata/hr mpg
2024-01-28 07:39:42 -07:00
Hargata Softworks
4779d3f161 Merge pull request #181 from hargata/Hargata/move.records
Move records around.
2024-01-28 07:38:04 -07:00
DESKTOP-GENO133\IvanPlex
b0173fae94 added object inheritance. 2024-01-28 07:37:12 -07:00
DESKTOP-GENO133\IvanPlex
c6ee8830a3 added hours per gallon calculation. 2024-01-27 23:29:05 -07:00
DESKTOP-GENO133\IvanPlex
c2eeab5025 Merge commit '43fd40347f34bc7676bfd5531abf612e06285e5c' into Hargata/hr-mpg 2024-01-27 22:53:55 -07:00
DESKTOP-GENO133\IvanPlex
43fd40347f added engine hours variable. 2024-01-27 21:14:29 -07:00
Hargata Softworks
53139f9bb2 Merge pull request #179 from hargata/Hargata/sorting.supplies
added sorting in supplies tab.
2024-01-27 12:17:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0b6033cc00 added sorting in supplies tab. 2024-01-27 12:16:49 -07:00
Hargata Softworks
6f0115e5c5 Merge pull request #176 from hargata/Hargata/pinned.notes.garage
touch screen support for pinned notes.
2024-01-27 07:41:06 -07:00
Hargata Softworks
2403adf537 Merge pull request #177 from hargata/Hargata/sort.columns
make cost columns sortable.
2024-01-27 07:40:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f00ab897b5 make cost columns sortable. 2024-01-27 07:37:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
093631bf6f touch screen support for pinned notes. 2024-01-26 13:19:37 -07:00
Hargata Softworks
5c2835ab76 Merge pull request #175 from hargata/Hargata/pinned.notes.garage
added maxfilesize label and error handling.
2024-01-26 12:42:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
03029981fd added maxfilesize label and error handling. 2024-01-26 12:40:39 -07:00
Hargata Softworks
69b1838038 Merge pull request #174 from hargata/Hargata/pinned.notes.garage
display pinned notes in garage.
2024-01-26 12:02:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1c2f83026c display pinned notes in garage. 2024-01-26 12:01:53 -07:00
Hargata Softworks
d36f2c59e3 Merge pull request #172 from hargata/Hargata/demo.stuff
clear temp files restoring demo instance.
2024-01-26 09:26:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
53ebec3f03 clear temp files restoring demo instance. 2024-01-26 09:25:44 -07:00
Hargata Softworks
f2cbbaeb12 Merge pull request #170 from hargata/Hargata/average.fuel.fix
include partial fuel up fuel consumption in average mpg.
2024-01-25 22:19:02 -07:00
DESKTOP-GENO133\IvanPlex
31c1202649 include partial fuel up fuel consumption in average mpg. 2024-01-25 22:18:33 -07:00
Hargata Softworks
331499461a Merge pull request #148 from hargata/Hargata/documentation
Hargata/documentation
2024-01-25 17:52:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8c6dd5e343 Updated website 2024-01-25 17:52:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
01b1e8228d Merge branch 'main' into Hargata/documentation 2024-01-25 17:01:31 -07:00
Hargata Softworks
43979d6115 Merge pull request #167 from hargata/Hargata/demo.mode
Demo Mode
2024-01-25 15:02:20 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6a9860e202 added demo mode. 2024-01-25 15:01:56 -07:00
Hargata Softworks
39338e8028 Merge pull request #166 from hargata/Hargata/decimal.odometer
added more screenshots
2024-01-25 14:23:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d1d5351a01 added more screenshots 2024-01-25 14:22:21 -07:00
Hargata Softworks
c9385c7fdd Merge pull request #165 from hargata/Hargata/decimal.odometer
updated website.
2024-01-25 14:06:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
afaae89af6 updated website. 2024-01-25 14:05:59 -07:00
Hargata Softworks
b9d799cd49 Merge pull request #164 from hargata/Hargata/decimal.odometer
Prevent decimals in odometer fields to error out.
2024-01-25 13:59:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e47c541e08 Prevent decimals in odometer fields to error out. 2024-01-25 13:58:47 -07:00
Hargata Softworks
51ff01d2cd Merge pull request #162 from hargata/Hargata/locale.gas.fix
more gas fixes due to european locale.
2024-01-25 08:33:25 -07:00
DESKTOP-GENO133\IvanPlex
618399cb09 more gas fixes due to european locale. 2024-01-25 08:31:56 -07:00
Hargata Softworks
b837a2e528 Merge pull request #158 from hargata/Hargata/reminder.email.endpoint
stop method from returning prematurely.
2024-01-24 22:27:32 -07:00
DESKTOP-GENO133\IvanPlex
a1e8b8f9cc stop method from returning prematurely. 2024-01-24 22:26:55 -07:00
Hargata Softworks
787c5da72a Merge pull request #157 from hargata/Hargata/reminder.email.endpoint
added vehicle information.
2024-01-24 22:24:40 -07:00
DESKTOP-GENO133\IvanPlex
260703be8e added vehicle information. 2024-01-24 22:23:31 -07:00
Hargata Softworks
053801b046 Merge pull request #156 from hargata/Hargata/reminder.email.endpoint
Hargata/reminder.email.endpoint
2024-01-24 21:48:15 -07:00
DESKTOP-GENO133\IvanPlex
db9b1970c5 updated API documentation. 2024-01-24 21:47:05 -07:00
DESKTOP-GENO133\IvanPlex
b153ef5ea5 added more mileage intervals and api endpoint to send out reminder emails. 2024-01-24 21:43:26 -07:00
Hargata Softworks
b54809f399 Merge pull request #155 from hargata/Hargata/zero.chart
Baseline zero charges for bar charts.
2024-01-24 09:07:33 -07:00
DESKTOP-GENO133\IvanPlex
f7f47c54ff added baseline zero costs charge so that bar charts display all months regardless of whether there are costs or not. 2024-01-24 09:03:55 -07:00
Hargata Softworks
92564ae527 Merge pull request #151 from hargata/Hargata/recurring.tax
Hargata/recurring.tax
2024-01-23 22:41:51 -07:00
DESKTOP-GENO133\IvanPlex
52ada8574d add pinned icon for pinned notes. 2024-01-23 22:15:53 -07:00
DESKTOP-GENO133\IvanPlex
013fb67943 Recurring fees. 2024-01-23 21:48:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d86298f502 make tax recurring. 2024-01-23 21:10:59 -07:00
Hargata Softworks
5891b78be0 Merge pull request #149 from hargata/Hargata/pin.notes
added ability to pin notes so that they always show up on top.
2024-01-23 17:48:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a9e3e44f2c added ability to pin notes so that they always show up on top. 2024-01-23 17:46:54 -07:00
Hargata Softworks
0d1c7234e8 Create Collaboration.md 2024-01-23 17:22:07 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e3abe5f209 Merge branch 'main' into Hargata/documentation 2024-01-23 17:15:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b453bfce5b removed auth html. 2024-01-23 17:14:53 -07:00
Hargata Softworks
147a1b03a7 Create Registration.md 2024-01-23 16:57:02 -07:00
Hargata Softworks
3017db5f86 Update Configuring.md 2024-01-23 16:45:34 -07:00
Hargata Softworks
399d0d8058 Update GettingStarted.md 2024-01-23 16:43:52 -07:00
Hargata Softworks
b8c0d4ef67 Update GettingStarted.md 2024-01-23 16:43:03 -07:00
Hargata Softworks
918d086705 Update Configuring.md 2024-01-23 16:39:40 -07:00
Hargata Softworks
ac05acf96b Update Configuring.md 2024-01-23 16:37:49 -07:00
Hargata Softworks
e7449806c0 Create Configuring.md 2024-01-23 16:36:46 -07:00
Hargata Softworks
baab3213b5 Update GettingStarted.md 2024-01-23 16:19:41 -07:00
Hargata Softworks
d68e9e9589 Create GettingStarted.md 2024-01-23 16:16:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
dbb139dfad updated documentation. 2024-01-17 12:00:05 -07:00
126 changed files with 4605 additions and 992 deletions

1
.env
View File

@@ -6,3 +6,4 @@ MailConfig__UseSSL="false"
MailConfig__Port=587 MailConfig__Port=587
MailConfig__Username="" MailConfig__Username=""
MailConfig__Password="" MailConfig__Password=""
LOGGING__LOGLEVEL__DEFAULT=Error

View File

@@ -0,0 +1,51 @@
name: Build and Push Image to Dockerhub and GHCR
on:
push:
branches: ["main"]
release:
types: ["published"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: "${{ secrets.DH_USER }}"
password: "${{ secrets.DH_PASS }}"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: "hargata"
password: "${{ secrets.GHCR_PAT }}"
- name: Docker Metadata
id: meta
uses: docker/metadata-action@v5
with:
context: workflow
images: |
hargata/lubelogger
ghcr.io/hargata/lubelogger
tags: |
type=edge,branch=main
type=ref,event=tag
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,30 +0,0 @@
name: Docker Image To Docker Hub
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: "${{ secrets.DH_USER }}"
password: "${{ secrets.DH_PASS }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
hargata/lubelogger:latest

View File

@@ -1,31 +0,0 @@
name: Docker Image To GHCR
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: "hargata"
password: "${{ secrets.GHCR_PAT }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/hargata/lubelogger:latest

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ data/cartracker.db
wwwroot/documents/ wwwroot/documents/
wwwroot/temp/ wwwroot/temp/
wwwroot/imports/ wwwroot/imports/
wwwroot/translations/

View File

@@ -22,10 +22,14 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess; private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper; private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper; private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic; private readonly IUserLogic _userLogic;
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
private readonly IMailHelper _mailHelper;
private readonly IConfigHelper _config;
public APIController(IVehicleDataAccess dataAccess, public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper, IGasHelper gasHelper,
IReminderHelper reminderHelper, IReminderHelper reminderHelper,
@@ -37,7 +41,11 @@ namespace CarCareTracker.Controllers
IReminderRecordDataAccess reminderRecordDataAccess, IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess, IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess, IOdometerRecordDataAccess odometerRecordDataAccess,
IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper,
IFileHelper fileHelper, IFileHelper fileHelper,
IConfigHelper config,
IUserLogic userLogic) IUserLogic userLogic)
{ {
_dataAccess = dataAccess; _dataAccess = dataAccess;
@@ -49,10 +57,14 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess; _reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess; _upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess; _odometerRecordDataAccess = odometerRecordDataAccess;
_userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper;
_gasHelper = gasHelper; _gasHelper = gasHelper;
_reminderHelper = reminderHelper; _reminderHelper = reminderHelper;
_userLogic = userLogic; _userLogic = userLogic;
_fileHelper = fileHelper; _fileHelper = fileHelper;
_config = config;
} }
public IActionResult Index() public IActionResult Index()
{ {
@@ -117,6 +129,17 @@ namespace CarCareTracker.Controllers
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost)
}; };
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
var odometerRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Mileage = int.Parse(input.Odometer)
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
}
response.Success = true; response.Success = true;
response.Message = "Service Record Added"; response.Message = "Service Record Added";
return Json(response); return Json(response);
@@ -173,6 +196,17 @@ namespace CarCareTracker.Controllers
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost)
}; };
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
var odometerRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Mileage = int.Parse(input.Odometer)
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
}
response.Success = true; response.Success = true;
response.Message = "Repair Record Added"; response.Message = "Repair Record Added";
return Json(response); return Json(response);
@@ -229,6 +263,17 @@ namespace CarCareTracker.Controllers
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost)
}; };
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
var odometerRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Mileage = int.Parse(input.Odometer)
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
}
response.Success = true; response.Success = true;
response.Message = "Upgrade Record Added"; response.Message = "Upgrade Record Added";
return Json(response); return Json(response);
@@ -404,6 +449,17 @@ namespace CarCareTracker.Controllers
Cost = decimal.Parse(input.Cost) Cost = decimal.Parse(input.Cost)
}; };
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord); _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
{
var odometerRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Mileage = int.Parse(input.Odometer)
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
}
response.Success = true; response.Success = true;
response.Message = "Gas Record Added"; response.Message = "Gas Record Added";
return Json(response); return Json(response);
@@ -428,12 +484,65 @@ namespace CarCareTracker.Controllers
} }
[Authorize(Roles = nameof(UserData.IsRootUser))] [Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet] [HttpGet]
[Route("/api/vehicle/reminders/send")]
public IActionResult SendReminders(List<ReminderUrgency> urgencies)
{
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
foreach(Vehicle vehicle in vehicles)
{
var vehicleId = vehicle.Id;
//get reminders
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).OrderByDescending(x => x.Urgency).ToList();
results.RemoveAll(x => !urgencies.Contains(x.Urgency));
if (!results.Any())
{
continue;
}
//get list of recipients.
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
List<string> emailRecipients = new List<string>();
foreach (int userId in userIds)
{
var userData = _userRecordDataAccess.GetUserRecordById(userId);
emailRecipients.Add(userData.EmailAddress);
};
if (!emailRecipients.Any())
{
continue;
}
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
operationResponses.Add(result);
}
if (operationResponses.All(x => x.Success))
{
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
} else if (operationResponses.All(x => !x.Success))
{
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
} else
{
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/makebackup")] [Route("/api/makebackup")]
public IActionResult MakeBackup() public IActionResult MakeBackup()
{ {
var result = _fileHelper.MakeBackup(); var result = _fileHelper.MakeBackup();
return Json(result); return Json(result);
} }
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/demo/restore")]
public IActionResult RestoreDemo()
{
var result = _fileHelper.RestoreBackup("/defaults/demo_default.zip", true);
return Json(result);
}
private int GetMaxMileage(int vehicleId) private int GetMaxMileage(int vehicleId)
{ {
var numbersArray = new List<int>(); var numbersArray = new List<int>();

View File

@@ -33,6 +33,26 @@ namespace CarCareTracker.Controllers
return Json(fileName); return Json(fileName);
} }
[HttpPost]
public IActionResult HandleTranslationFileUpload(IFormFile file)
{
var originalFileName = Path.GetFileNameWithoutExtension(file.FileName);
if (originalFileName == "en_US")
{
return Json(new OperationResponse { Success = false, Message = "The translation file name en_US is reserved." });
}
var fileName = UploadFile(file);
//move file from temp to translation folder.
var uploadedFilePath = _fileHelper.MoveFileFromTemp(fileName, "translations/");
//rename uploaded file so that it preserves original name.
if (!string.IsNullOrWhiteSpace(uploadedFilePath))
{
var result = _fileHelper.RenameFile(uploadedFilePath, originalFileName);
return Json(new OperationResponse { Success = result, Message = string.Empty });
}
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
[HttpPost] [HttpPost]
public IActionResult HandleMultipleFileUpload(List<IFormFile> file) public IActionResult HandleMultipleFileUpload(List<IFormFile> file)
{ {
@@ -44,7 +64,7 @@ namespace CarCareTracker.Controllers
} }
return Json(uploadedFiles); return Json(uploadedFiles);
} }
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost] [HttpPost]
public IActionResult DeleteFiles(string fileLocation) public IActionResult DeleteFiles(string fileLocation)
{ {

View File

@@ -15,17 +15,20 @@ namespace CarCareTracker.Controllers
private readonly ILogger<HomeController> _logger; private readonly ILogger<HomeController> _logger;
private readonly IVehicleDataAccess _dataAccess; private readonly IVehicleDataAccess _dataAccess;
private readonly IUserLogic _userLogic; private readonly IUserLogic _userLogic;
private readonly IFileHelper _fileHelper;
private readonly IConfigHelper _config; private readonly IConfigHelper _config;
public HomeController(ILogger<HomeController> logger, public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess, IVehicleDataAccess dataAccess,
IUserLogic userLogic, IUserLogic userLogic,
IConfigHelper configuration) IConfigHelper configuration,
IFileHelper fileHelper)
{ {
_logger = logger; _logger = logger;
_dataAccess = dataAccess; _dataAccess = dataAccess;
_config = configuration; _config = configuration;
_userLogic = userLogic; _userLogic = userLogic;
_fileHelper = fileHelper;
} }
private int GetUserID() private int GetUserID()
{ {
@@ -47,7 +50,13 @@ namespace CarCareTracker.Controllers
public IActionResult Settings() public IActionResult Settings()
{ {
var userConfig = _config.GetUserConfig(User); var userConfig = _config.GetUserConfig(User);
return PartialView("_Settings", userConfig); var languages = _fileHelper.GetLanguages();
var viewModel = new SettingsViewModel
{
UserConfig = userConfig,
UILanguages = languages
};
return PartialView("_Settings", viewModel);
} }
[HttpPost] [HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig) public IActionResult WriteToSettings(UserConfig userConfig)

View File

@@ -26,9 +26,9 @@ namespace CarCareTracker.Controllers
_logger = logger; _logger = logger;
_loginLogic = loginLogic; _loginLogic = loginLogic;
} }
public IActionResult Index() public IActionResult Index(string redirectURL = "")
{ {
return View(); return View(model: redirectURL);
} }
public IActionResult Registration() public IActionResult Registration()
{ {

View File

@@ -27,6 +27,7 @@ namespace CarCareTracker.Controllers
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess; private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
private readonly IPlanRecordDataAccess _planRecordDataAccess; private readonly IPlanRecordDataAccess _planRecordDataAccess;
private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IWebHostEnvironment _webEnv; private readonly IWebHostEnvironment _webEnv;
private readonly IConfigHelper _config; private readonly IConfigHelper _config;
@@ -51,6 +52,7 @@ namespace CarCareTracker.Controllers
IUpgradeRecordDataAccess upgradeRecordDataAccess, IUpgradeRecordDataAccess upgradeRecordDataAccess,
ISupplyRecordDataAccess supplyRecordDataAccess, ISupplyRecordDataAccess supplyRecordDataAccess,
IPlanRecordDataAccess planRecordDataAccess, IPlanRecordDataAccess planRecordDataAccess,
IPlanRecordTemplateDataAccess planRecordTemplateDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess, IOdometerRecordDataAccess odometerRecordDataAccess,
IUserLogic userLogic, IUserLogic userLogic,
IWebHostEnvironment webEnv, IWebHostEnvironment webEnv,
@@ -71,6 +73,7 @@ namespace CarCareTracker.Controllers
_upgradeRecordDataAccess = upgradeRecordDataAccess; _upgradeRecordDataAccess = upgradeRecordDataAccess;
_supplyRecordDataAccess = supplyRecordDataAccess; _supplyRecordDataAccess = supplyRecordDataAccess;
_planRecordDataAccess = planRecordDataAccess; _planRecordDataAccess = planRecordDataAccess;
_planRecordTemplateDataAccess = planRecordTemplateDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess; _odometerRecordDataAccess = odometerRecordDataAccess;
_userLogic = userLogic; _userLogic = userLogic;
_webEnv = webEnv; _webEnv = webEnv;
@@ -85,6 +88,7 @@ namespace CarCareTracker.Controllers
public IActionResult Index(int vehicleId) public IActionResult Index(int vehicleId)
{ {
var data = _dataAccess.GetVehicleById(vehicleId); var data = _dataAccess.GetVehicleById(vehicleId);
UpdateRecurringTaxes(vehicleId);
return View(data); return View(data);
} }
[HttpGet] [HttpGet]
@@ -141,6 +145,7 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) && _reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) && _upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
_planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) && _planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) &&
_planRecordTemplateDataAccess.DeleteAllPlanRecordTemplatesByVehicleId(vehicleId) &&
_supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) && _supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) &&
_userLogic.DeleteAllAccessToVehicle(vehicleId) && _userLogic.DeleteAllAccessToVehicle(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId); _dataAccess.DeleteVehicle(vehicleId);
@@ -171,7 +176,7 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
@@ -189,7 +194,7 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId); var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
@@ -207,7 +212,7 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
@@ -225,7 +230,7 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Notes = x.Notes, Odometer = x.Mileage.ToString() }); var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), Tags = string.Join(" ", x.Tags) });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
@@ -270,7 +275,7 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
if (vehicleRecords.Any()) if (vehicleRecords.Any())
{ {
var exportData = vehicleRecords.Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes }); var exportData = vehicleRecords.Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Tags = string.Join(" ", x.Tags) });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
@@ -316,7 +321,6 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG; bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG; bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
vehicleRecords = vehicleRecords.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG); var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
var exportData = convertedRecords.Select(x => new GasRecordExportModel var exportData = convertedRecords.Select(x => new GasRecordExportModel
{ {
@@ -327,7 +331,8 @@ namespace CarCareTracker.Controllers
Odometer = x.Mileage.ToString(), Odometer = x.Mileage.ToString(),
IsFillToFull = x.IsFillToFull.ToString(), IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(), MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes Notes = x.Notes,
Tags = string.Join(" ", x.Tags)
}); });
using (var writer = new StreamWriter(fullExportFilePath)) using (var writer = new StreamWriter(fullExportFilePath))
{ {
@@ -376,9 +381,10 @@ namespace CarCareTracker.Controllers
{ {
VehicleId = vehicleId, VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any), Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price)) if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{ {
@@ -420,10 +426,11 @@ namespace CarCareTracker.Controllers
{ {
VehicleId = vehicleId, VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any) Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
} }
@@ -433,8 +440,9 @@ namespace CarCareTracker.Controllers
{ {
VehicleId = vehicleId, VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord); _odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
} }
@@ -463,10 +471,11 @@ namespace CarCareTracker.Controllers
{ {
VehicleId = vehicleId, VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any) Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
} }
@@ -476,10 +485,11 @@ namespace CarCareTracker.Controllers
{ {
VehicleId = vehicleId, VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any) Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord); _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
} }
@@ -506,7 +516,8 @@ namespace CarCareTracker.Controllers
Date = DateTime.Parse(importModel.Date), Date = DateTime.Parse(importModel.Date),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description, Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any) Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
}; };
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord); _taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
} }
@@ -529,8 +540,6 @@ namespace CarCareTracker.Controllers
public IActionResult GetGasRecordsByVehicleId(int vehicleId) public IActionResult GetGasRecordsByVehicleId(int vehicleId)
{ {
var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
//need it in ascending order to perform computation.
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
//check if the user uses MPG or Liters per 100km. //check if the user uses MPG or Liters per 100km.
var userConfig = _config.GetUserConfig(User); var userConfig = _config.GetUserConfig(User);
bool useMPG = userConfig.UseMPG; bool useMPG = userConfig.UseMPG;
@@ -540,10 +549,13 @@ namespace CarCareTracker.Controllers
{ {
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList(); computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
} }
var vehicleIsElectric = _dataAccess.GetVehicleById(vehicleId).IsElectric; var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
var viewModel = new GasRecordViewModelContainer() var viewModel = new GasRecordViewModelContainer()
{ {
UseKwh = vehicleIsElectric, UseKwh = vehicleIsElectric,
UseHours = vehicleUseHours,
GasRecords = computedResults GasRecords = computedResults
}; };
return PartialView("_Gas", viewModel); return PartialView("_Gas", viewModel);
@@ -566,9 +578,12 @@ namespace CarCareTracker.Controllers
return Json(result); return Json(result);
} }
[HttpGet] [HttpGet]
public IActionResult GetAddGasRecordPartialView() public IActionResult GetAddGasRecordPartialView(int vehicleId)
{ {
return PartialView("_GasModal", new GasRecordInputContainer() { GasRecord = new GasRecordInput() }); var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
return PartialView("_GasModal", new GasRecordInputContainer() { UseKwh = vehicleIsElectric, UseHours = vehicleUseHours, GasRecord = new GasRecordInput() });
} }
[HttpGet] [HttpGet]
public IActionResult GetGasRecordForEditById(int gasRecordId) public IActionResult GetGasRecordForEditById(int gasRecordId)
@@ -585,12 +600,16 @@ namespace CarCareTracker.Controllers
Gallons = result.Gallons, Gallons = result.Gallons,
IsFillToFull = result.IsFillToFull, IsFillToFull = result.IsFillToFull,
MissedFuelUp = result.MissedFuelUp, MissedFuelUp = result.MissedFuelUp,
Notes = result.Notes Notes = result.Notes,
Tags = result.Tags
}; };
var vehicleIsElectric = _dataAccess.GetVehicleById(convertedResult.VehicleId).IsElectric; var vehicleData = _dataAccess.GetVehicleById(convertedResult.VehicleId);
var vehicleIsElectric = vehicleData.IsElectric;
var vehicleUseHours = vehicleData.UseHours;
var viewModel = new GasRecordInputContainer() var viewModel = new GasRecordInputContainer()
{ {
UseKwh = vehicleIsElectric, UseKwh = vehicleIsElectric,
UseHours = vehicleUseHours,
GasRecord = convertedResult GasRecord = convertedResult
}; };
return PartialView("_GasModal", viewModel); return PartialView("_GasModal", viewModel);
@@ -601,6 +620,15 @@ namespace CarCareTracker.Controllers
var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId); var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId);
return Json(result); return Json(result);
} }
[HttpPost]
public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit)
{
var currentConfig = _config.GetUserConfig(User);
currentConfig.PreferredGasUnit = gasUnit;
currentConfig.PreferredGasMileageUnit = fuelMileageUnit;
var result = _config.SaveUserConfig(User, currentConfig);
return Json(result);
}
#endregion #endregion
#region "Service Records" #region "Service Records"
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
@@ -660,7 +688,8 @@ namespace CarCareTracker.Controllers
Mileage = result.Mileage, Mileage = result.Mileage,
Notes = result.Notes, Notes = result.Notes,
VehicleId = result.VehicleId, VehicleId = result.VehicleId,
Files = result.Files Files = result.Files,
Tags = result.Tags
}; };
return PartialView("_ServiceRecordModal", convertedResult); return PartialView("_ServiceRecordModal", convertedResult);
} }
@@ -729,7 +758,8 @@ namespace CarCareTracker.Controllers
Mileage = result.Mileage, Mileage = result.Mileage,
Notes = result.Notes, Notes = result.Notes,
VehicleId = result.VehicleId, VehicleId = result.VehicleId,
Files = result.Files Files = result.Files,
Tags = result.Tags
}; };
return PartialView("_CollisionRecordModal", convertedResult); return PartialView("_CollisionRecordModal", convertedResult);
} }
@@ -757,6 +787,35 @@ namespace CarCareTracker.Controllers
} }
return PartialView("_TaxRecords", result); return PartialView("_TaxRecords", result);
} }
private void UpdateRecurringTaxes(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var recurringFees = result.Where(x => x.IsRecurring);
if (recurringFees.Any())
{
foreach(TaxRecord recurringFee in recurringFees)
{
var newDate = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
if (DateTime.Now > newDate){
recurringFee.IsRecurring = false;
var newRecurringFee = new TaxRecord()
{
VehicleId = recurringFee.VehicleId,
Date = newDate,
Description = recurringFee.Description,
Cost = recurringFee.Cost,
IsRecurring = true,
Notes = recurringFee.Notes,
RecurringInterval = recurringFee.RecurringInterval,
Files = recurringFee.Files,
Tags = recurringFee.Tags
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
}
}
}
}
[HttpPost] [HttpPost]
public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord) public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord)
{ {
@@ -783,7 +842,10 @@ namespace CarCareTracker.Controllers
Description = result.Description, Description = result.Description,
Notes = result.Notes, Notes = result.Notes,
VehicleId = result.VehicleId, VehicleId = result.VehicleId,
Files = result.Files IsRecurring = result.IsRecurring,
RecurringInterval = result.RecurringInterval,
Files = result.Files,
Tags = result.Tags
}; };
return PartialView("_TaxRecordModal", convertedResult); return PartialView("_TaxRecordModal", convertedResult);
} }
@@ -816,7 +878,7 @@ namespace CarCareTracker.Controllers
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost) UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
}; };
//get costbymonth //get costbymonth
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>(); List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0)); allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0)); allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0)); allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
@@ -867,10 +929,17 @@ namespace CarCareTracker.Controllers
var userConfig = _config.GetUserConfig(User); var userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG); var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default); mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{ {
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), MonthId = x.Key,
Cost = x.Average(y => y.MilesPerGallon) Cost = x.Average(y => y.MilesPerGallon)
}));
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y=>y.Cost)
}).ToList(); }).ToList();
viewModel.FuelMileageForVehicleByMonth = monthlyMileageData; viewModel.FuelMileageForVehicleByMonth = monthlyMileageData;
return PartialView("_Report", viewModel); return PartialView("_Report", viewModel);
@@ -937,6 +1006,84 @@ namespace CarCareTracker.Controllers
return PartialView("_ReminderMakeUpReport", viewModel); return PartialView("_ReminderMakeUpReport", viewModel);
} }
[TypeFilter(typeof(CollaboratorFilter))] [TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetVehicleAttachments(int vehicleId, List<ImportMode> exportTabs)
{
List<GenericReportModel> attachmentData = new List<GenericReportModel>();
if (exportTabs.Contains(ImportMode.ServiceRecord)){
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x=>x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.RepairRecord))
{
var records = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.UpgradeRecord))
{
var records = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.GasRecord))
{
var records = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.TaxRecord))
{
var records = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = 0,
Files = x.Files
}));
}
if (exportTabs.Contains(ImportMode.OdometerRecord))
{
var records = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
attachmentData.AddRange(records.Select(x => new GenericReportModel
{
Date = x.Date,
Odometer = x.Mileage,
Files = x.Files
}));
}
if (attachmentData.Any())
{
attachmentData = attachmentData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList();
var result = _fileHelper.MakeAttachmentsExport(attachmentData);
if (string.IsNullOrWhiteSpace(result))
{
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
}
return Json(new OperationResponse { Success = true, Message = result });
} else
{
return Json(new OperationResponse { Success = false, Message = "No Attachments Found" });
}
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetVehicleHistory(int vehicleId) public IActionResult GetVehicleHistory(int vehicleId)
{ {
var vehicleHistory = new VehicleHistoryViewModel(); var vehicleHistory = new VehicleHistoryViewModel();
@@ -950,15 +1097,28 @@ namespace CarCareTracker.Controllers
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG; bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG; bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost); vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost);
vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost); vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost);
var averageMPG = "0"; var averageMPG = "0";
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG); var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
if (gasViewModels.Any()) if (gasViewModels.Any())
{ {
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels); averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG);
} }
vehicleHistory.MPG = averageMPG; var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleHistory.VehicleData.IsElectric, vehicleHistory.VehicleData.UseHours, useMPG, useUKMPG);
if (fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l")
{
//conversion needed.
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
if (newAverageMPG != 0)
{
newAverageMPG = 100 / newAverageMPG;
}
averageMPG = newAverageMPG.ToString("F");
fuelEconomyMileageUnit = preferredFuelMileageUnit;
}
vehicleHistory.MPG = $"{averageMPG} {fuelEconomyMileageUnit}";
//insert servicerecords //insert servicerecords
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
{ {
@@ -1012,10 +1172,17 @@ namespace CarCareTracker.Controllers
mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year); mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year);
} }
mileageData.RemoveAll(x => x.MilesPerGallon == default); mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{ {
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), MonthId = x.Key,
Cost = x.Average(y => y.MilesPerGallon) Cost = x.Average(y => y.MilesPerGallon)
}));
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
}).ToList(); }).ToList();
return PartialView("_MPGByMonthReport", monthlyMileageData); return PartialView("_MPGByMonthReport", monthlyMileageData);
} }
@@ -1023,7 +1190,7 @@ namespace CarCareTracker.Controllers
[HttpPost] [HttpPost]
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0) public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
{ {
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>(); List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
if (selectedMetrics.Contains(ImportMode.ServiceRecord)) if (selectedMetrics.Contains(ImportMode.ServiceRecord))
{ {
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
@@ -1180,7 +1347,8 @@ namespace CarCareTracker.Controllers
Metric = result.Metric, Metric = result.Metric,
IsRecurring = result.IsRecurring, IsRecurring = result.IsRecurring,
ReminderMileageInterval = result.ReminderMileageInterval, ReminderMileageInterval = result.ReminderMileageInterval,
ReminderMonthInterval = result.ReminderMonthInterval ReminderMonthInterval = result.ReminderMonthInterval,
CustomMileageInterval = result.CustomMileageInterval
}; };
return PartialView("_ReminderRecordModal", convertedResult); return PartialView("_ReminderRecordModal", convertedResult);
} }
@@ -1249,7 +1417,8 @@ namespace CarCareTracker.Controllers
Mileage = result.Mileage, Mileage = result.Mileage,
Notes = result.Notes, Notes = result.Notes,
VehicleId = result.VehicleId, VehicleId = result.VehicleId,
Files = result.Files Files = result.Files,
Tags = result.Tags
}; };
return PartialView("_UpgradeRecordModal", convertedResult); return PartialView("_UpgradeRecordModal", convertedResult);
} }
@@ -1266,8 +1435,17 @@ namespace CarCareTracker.Controllers
public IActionResult GetNotesByVehicleId(int vehicleId) public IActionResult GetNotesByVehicleId(int vehicleId)
{ {
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId); var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.OrderByDescending(x => x.Pinned).ToList();
return PartialView("_Notes", result); return PartialView("_Notes", result);
} }
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
{
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
result = result.Where(x=>x.Pinned).ToList();
return Json(result);
}
[HttpPost] [HttpPost]
public IActionResult SaveNoteToVehicleId(Note note) public IActionResult SaveNoteToVehicleId(Note note)
{ {
@@ -1293,6 +1471,21 @@ namespace CarCareTracker.Controllers
} }
#endregion #endregion
#region "Supply Records" #region "Supply Records"
private List<string> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
{
//returns empty string if all supplies are available
var result = new List<string>();
foreach (SupplyUsage supply in supplyUsage)
{
//get supply record.
var supplyData = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
if (supply.Quantity > supplyData.Quantity)
{
result.Add($"Insufficient Quantity for {supplyData.Description}, need: {supply.Quantity}, available: {supplyData.Quantity}");
}
}
return result;
}
private void RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage) private void RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage)
{ {
foreach(SupplyUsage supply in supplyUsage) foreach(SupplyUsage supply in supplyUsage)
@@ -1407,6 +1600,60 @@ namespace CarCareTracker.Controllers
} }
return Json(result); return Json(result);
} }
[HttpPost]
public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord)
{
//check if template name already taken.
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x=>x.Description == planRecord.Description).Any();
if (existingRecord)
{
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle"});
}
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
return Json(new OperationResponse { Success = result, Message = result ? "Template Added" : StaticHelper.GenericErrorMessage });
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetPlanRecordTemplatesForVehicleId(int vehicleId)
{
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicleId);
return PartialView("_PlanRecordTemplateModal", result);
}
[HttpPost]
public IActionResult DeletePlanRecordTemplateById(int planRecordTemplateId)
{
var result = _planRecordTemplateDataAccess.DeletePlanRecordTemplateById(planRecordTemplateId);
return Json(result);
}
[HttpPost]
public IActionResult ConvertPlanRecordTemplateToPlanRecord(int planRecordTemplateId)
{
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
if (existingRecord.Id == default)
{
return Json(new OperationResponse { Success = false, Message = "Unable to find template" });
}
if (existingRecord.Supplies.Any())
{
//check if all supplies are available
var supplyAvailability = CheckSupplyRecordsAvailability(existingRecord.Supplies);
if (supplyAvailability.Any())
{
return Json(new OperationResponse { Success = false, Message = string.Join("<br>", supplyAvailability) });
}
}
//populate createdDate
existingRecord.DateCreated = DateTime.Now.ToString("G");
existingRecord.DateModified = DateTime.Now.ToString("G");
existingRecord.Id = default;
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
if (result && existingRecord.Supplies.Any())
{
RequisitionSupplyRecordsByUsage(existingRecord.Supplies);
}
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
}
[HttpGet] [HttpGet]
public IActionResult GetAddPlanRecordPartialView() public IActionResult GetAddPlanRecordPartialView()
{ {
@@ -1547,7 +1794,8 @@ namespace CarCareTracker.Controllers
Mileage = result.Mileage, Mileage = result.Mileage,
Notes = result.Notes, Notes = result.Notes,
VehicleId = result.VehicleId, VehicleId = result.VehicleId,
Files = result.Files Files = result.Files,
Tags = result.Tags
}; };
return PartialView("_OdometerRecordModal", convertedResult); return PartialView("_OdometerRecordModal", convertedResult);
} }
@@ -1558,5 +1806,55 @@ namespace CarCareTracker.Controllers
return Json(result); return Json(result);
} }
#endregion #endregion
#region "Shared Methods"
public IActionResult MoveRecord(int recordId, ImportMode source, ImportMode destination)
{
var genericRecord = new GenericRecord();
bool result = false;
//get
switch (source)
{
case ImportMode.ServiceRecord:
genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
genericRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
break;
}
//save
switch (destination)
{
case ImportMode.ServiceRecord:
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord));
break;
case ImportMode.RepairRecord:
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(StaticHelper.GenericToRepairRecord(genericRecord));
break;
case ImportMode.UpgradeRecord:
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord));
break;
}
//delete
if (result)
{
switch (source)
{
case ImportMode.ServiceRecord:
_serviceRecordDataAccess.DeleteServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
_collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
_upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
break;
}
}
return Json(result);
}
#endregion
} }
} }

View File

@@ -2,6 +2,7 @@
{ {
public enum ReminderMileageInterval public enum ReminderMileageInterval
{ {
Other = 0,
FiftyMiles = 50, FiftyMiles = 50,
OneHundredMiles = 100, OneHundredMiles = 100,
FiveHundredMiles = 500, FiveHundredMiles = 500,
@@ -14,7 +15,9 @@
FifteenThousandMiles = 15000, FifteenThousandMiles = 15000,
TwentyThousandMiles = 20000, TwentyThousandMiles = 20000,
ThirtyThousandMiles = 30000, ThirtyThousandMiles = 30000,
FortyThousandMiles = 40000,
FiftyThousandMiles = 50000, FiftyThousandMiles = 50000,
SixtyThousandMiles = 60000,
OneHundredThousandMiles = 100000, OneHundredThousandMiles = 100000,
OneHundredFiftyThousandMiles = 150000 OneHundredFiftyThousandMiles = 150000
} }

View File

@@ -2,6 +2,7 @@
{ {
public enum ReminderMonthInterval public enum ReminderMonthInterval
{ {
OneMonth = 1,
ThreeMonths = 3, ThreeMonths = 3,
SixMonths = 6, SixMonths = 6,
OneYear = 12, OneYear = 12,

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class PlanRecordTemplateDataAccess : IPlanRecordTemplateDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "planrecordtemplates";
public List<PlanRecordInput> GetPlanRecordTemplatesByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecordInput>(tableName);
var planRecords = table.Find(Query.EQ(nameof(PlanRecordInput.VehicleId), vehicleId));
return planRecords.ToList() ?? new List<PlanRecordInput>();
};
}
public PlanRecordInput GetPlanRecordTemplateById(int planRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecordInput>(tableName);
return table.FindById(planRecordId);
};
}
public bool DeletePlanRecordTemplateById(int planRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecordInput>(tableName);
table.Delete(planRecordId);
return true;
};
}
public bool SavePlanRecordTemplateToVehicle(PlanRecordInput planRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecordInput>(tableName);
table.Upsert(planRecord);
return true;
};
}
public bool DeleteAllPlanRecordTemplatesByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
var planRecords = table.DeleteMany(Query.EQ(nameof(PlanRecordInput.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IPlanRecordTemplateDataAccess
{
public List<PlanRecordInput> GetPlanRecordTemplatesByVehicleId(int vehicleId);
public PlanRecordInput GetPlanRecordTemplateById(int planRecordId);
public bool DeletePlanRecordTemplateById(int planRecordId);
public bool SavePlanRecordTemplateToVehicle(PlanRecordInput planRecord);
public bool DeleteAllPlanRecordTemplatesByVehicleId(int vehicleId);
}
}

View File

@@ -9,6 +9,8 @@ namespace CarCareTracker.Helper
{ {
UserConfig GetUserConfig(ClaimsPrincipal user); UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData); bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
string GetLogoUrl();
string GetServerLanguage();
public bool DeleteUserConfig(int userId); public bool DeleteUserConfig(int userId);
} }
public class ConfigHelper : IConfigHelper public class ConfigHelper : IConfigHelper
@@ -24,6 +26,20 @@ namespace CarCareTracker.Helper
_userConfig = userConfig; _userConfig = userConfig;
_cache = memoryCache; _cache = memoryCache;
} }
public string GetLogoUrl()
{
var logoUrl = _config["LUBELOGGER_LOGO_URL"];
if (string.IsNullOrWhiteSpace(logoUrl))
{
logoUrl = "/defaults/lubelogger_logo.png";
}
return logoUrl;
}
public string GetServerLanguage()
{
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
return serverLanguage;
}
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData) public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
{ {
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier); var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
@@ -94,9 +110,13 @@ namespace CarCareTracker.Helper
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]), EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]), HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]), UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]), UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]), EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]), EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
PreferredGasMileageUnit = _config[nameof(UserConfig.PreferredGasMileageUnit)],
PreferredGasUnit = _config[nameof(UserConfig.PreferredGasUnit)],
UserLanguage = _config[nameof(UserConfig.UserLanguage)],
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(), VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)]) DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
}; };

View File

@@ -1,4 +1,5 @@
using System.IO.Compression; using CarCareTracker.Models;
using System.IO.Compression;
namespace CarCareTracker.Helper namespace CarCareTracker.Helper
{ {
@@ -6,9 +7,12 @@ namespace CarCareTracker.Helper
{ {
string GetFullFilePath(string currentFilePath, bool mustExist = true); string GetFullFilePath(string currentFilePath, bool mustExist = true);
string MoveFileFromTemp(string currentFilePath, string newFolder); string MoveFileFromTemp(string currentFilePath, string newFolder);
bool RenameFile(string currentFilePath, string newName);
bool DeleteFile(string currentFilePath); bool DeleteFile(string currentFilePath);
string MakeBackup(); string MakeBackup();
bool RestoreBackup(string fileName); bool RestoreBackup(string fileName, bool clearExisting = false);
string MakeAttachmentsExport(List<GenericReportModel> exportData);
List<string> GetLanguages();
} }
public class FileHelper : IFileHelper public class FileHelper : IFileHelper
{ {
@@ -19,6 +23,40 @@ namespace CarCareTracker.Helper
_webEnv = webEnv; _webEnv = webEnv;
_logger = logger; _logger = logger;
} }
public List<string> GetLanguages()
{
var languagePath = Path.Combine(_webEnv.WebRootPath, "translations");
var defaultList = new List<string>() { "en_US" };
if (Directory.Exists(languagePath))
{
var listOfLanguages = Directory.GetFiles(languagePath);
if (listOfLanguages.Any())
{
defaultList.AddRange(listOfLanguages.Select(x => Path.GetFileNameWithoutExtension(x)));
}
}
return defaultList;
}
public bool RenameFile(string currentFilePath, string newName)
{
var fullFilePath = GetFullFilePath(currentFilePath);
if (!string.IsNullOrWhiteSpace(fullFilePath))
{
try
{
var originalFileName = Path.GetFileNameWithoutExtension(fullFilePath);
var newFilePath = fullFilePath.Replace(originalFileName, newName);
File.Move(fullFilePath, newFilePath);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
return false;
}
public string GetFullFilePath(string currentFilePath, bool mustExist = true) public string GetFullFilePath(string currentFilePath, bool mustExist = true)
{ {
if (currentFilePath.StartsWith("/")) if (currentFilePath.StartsWith("/"))
@@ -38,7 +76,7 @@ namespace CarCareTracker.Helper
return string.Empty; return string.Empty;
} }
} }
public bool RestoreBackup(string fileName) public bool RestoreBackup(string fileName, bool clearExisting = false)
{ {
var fullFilePath = GetFullFilePath(fileName); var fullFilePath = GetFullFilePath(fileName);
if (string.IsNullOrWhiteSpace(fullFilePath)) if (string.IsNullOrWhiteSpace(fullFilePath))
@@ -64,6 +102,14 @@ namespace CarCareTracker.Helper
{ {
Directory.CreateDirectory(existingPath); Directory.CreateDirectory(existingPath);
} }
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//copy each files from temp folder to newPath //copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(imagePath); var filesToUpload = Directory.GetFiles(imagePath);
foreach (string file in filesToUpload) foreach (string file in filesToUpload)
@@ -78,6 +124,14 @@ namespace CarCareTracker.Helper
{ {
Directory.CreateDirectory(existingPath); Directory.CreateDirectory(existingPath);
} }
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//copy each files from temp folder to newPath //copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(documentPath); var filesToUpload = Directory.GetFiles(documentPath);
foreach (string file in filesToUpload) foreach (string file in filesToUpload)
@@ -100,12 +154,37 @@ namespace CarCareTracker.Helper
File.Move(configPath, StaticHelper.UserConfigPath, true); File.Move(configPath, StaticHelper.UserConfigPath, true);
} }
return true; return true;
} catch (Exception ex) }
catch (Exception ex)
{ {
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}"); _logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
return false; return false;
} }
} }
public string MakeAttachmentsExport(List<GenericReportModel> exportData)
{
var folderName = Guid.NewGuid();
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
int fileIndex = 0;
foreach (GenericReportModel reportModel in exportData)
{
foreach (UploadedFiles file in reportModel.Files)
{
var fileToCopy = GetFullFilePath(file.Location);
var destFileName = $"{tempPath}/{fileIndex}{Path.GetExtension(file.Location)}";
File.Copy(fileToCopy, destFileName);
fileIndex++;
}
}
var destFilePath = $"{tempPath}.zip";
ZipFile.CreateFromDirectory(tempPath, destFilePath);
//delete temp directory
Directory.Delete(tempPath, true);
var zipFileName = $"/temp/{folderName}.zip";
return zipFileName;
}
public string MakeBackup() public string MakeBackup()
{ {
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}"; var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";

View File

@@ -5,26 +5,36 @@ namespace CarCareTracker.Helper
public interface IGasHelper public interface IGasHelper
{ {
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG); List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
string GetAverageGasMileage(List<GasRecordViewModel> results); string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG);
} }
public class GasHelper : IGasHelper public class GasHelper : IGasHelper
{ {
public string GetAverageGasMileage(List<GasRecordViewModel> results) public string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG)
{ {
var recordWithCalculatedMPG = results.Where(x => x.MilesPerGallon > 0); var recordsToCalculate = results.Where(x => x.IncludeInAverage);
var minMileage = results.Min(x => x.Mileage); if (recordsToCalculate.Any())
if (recordWithCalculatedMPG.Any())
{ {
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage); try
var totalGallonsConsumed = recordWithCalculatedMPG.Sum(x => x.Gallons); {
var deltaMileage = maxMileage - minMileage; var totalMileage = recordsToCalculate.Sum(x => x.DeltaMileage);
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed; var totalGallons = recordsToCalculate.Sum(x => x.Gallons);
var averageGasMileage = totalMileage / totalGallons;
if (!useMPG && averageGasMileage > 0)
{
averageGasMileage = 100 / averageGasMileage;
}
return averageGasMileage.ToString("F"); return averageGasMileage.ToString("F");
} catch (Exception ex)
{
return "0";
}
} }
return "0"; return "0";
} }
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG) public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
{ {
//need to order by to get correct results
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
var computedResults = new List<GasRecordViewModel>(); var computedResults = new List<GasRecordViewModel>();
int previousMileage = 0; int previousMileage = 0;
decimal unFactoredConsumption = 0.00M; decimal unFactoredConsumption = 0.00M;
@@ -60,7 +70,8 @@ namespace CarCareTracker.Helper
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0, CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
IsFillToFull = currentObject.IsFillToFull, IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp, MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes Notes = currentObject.Notes,
Tags = currentObject.Tags
}; };
if (currentObject.MissedFuelUp) if (currentObject.MissedFuelUp)
{ {
@@ -73,10 +84,17 @@ namespace CarCareTracker.Helper
else if (currentObject.IsFillToFull) else if (currentObject.IsFillToFull)
{ {
//if user filled to full. //if user filled to full.
if (convertedConsumption > 0.00M) if (convertedConsumption > 0.00M && deltaMileage > 0)
{
try
{ {
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption)); gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
} }
catch (Exception ex)
{
gasRecordViewModel.MilesPerGallon = 0;
}
}
//reset unFactored vars //reset unFactored vars
unFactoredConsumption = 0; unFactoredConsumption = 0;
unFactoredMileage = 0; unFactoredMileage = 0;
@@ -105,7 +123,8 @@ namespace CarCareTracker.Helper
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0, CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
IsFillToFull = currentObject.IsFillToFull, IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp, MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes Notes = currentObject.Notes,
Tags = currentObject.Tags
}); });
} }
previousMileage = currentObject.Mileage; previousMileage = currentObject.Mileage;

View File

@@ -8,15 +8,19 @@ namespace CarCareTracker.Helper
{ {
OperationResponse NotifyUserForRegistration(string emailAddress, string token); OperationResponse NotifyUserForRegistration(string emailAddress, string token);
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token); OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
} }
public class MailHelper : IMailHelper public class MailHelper : IMailHelper
{ {
private readonly MailConfig mailConfig; private readonly MailConfig mailConfig;
private readonly IFileHelper _fileHelper;
public MailHelper( public MailHelper(
IConfiguration config IConfiguration config,
IFileHelper fileHelper
) { ) {
//load mailConfig from Configuration //load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>(); mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
_fileHelper = fileHelper;
} }
public OperationResponse NotifyUserForRegistration(string emailAddress, string token) public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{ {
@@ -60,20 +64,67 @@ namespace CarCareTracker.Helper
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage }; return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
} }
} }
private bool SendEmail(string emailTo, string emailSubject, string emailBody) { public OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (!emailAddresses.Any())
{
return new OperationResponse { Success = false, Message = "No recipients could be found" };
}
if (!reminders.Any())
{
return new OperationResponse { Success = false, Message = "No reminders could be found" };
}
//get email template, this file has to exist since it's a static file.
var emailTemplatePath = _fileHelper.GetFullFilePath(StaticHelper.ReminderEmailTemplate);
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
//construct html table.
string emailBody = File.ReadAllText(emailTemplatePath);
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}");
string tableBody = "";
foreach(ReminderRecordViewModel reminder in reminders)
{
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
tableBody += $"<tr class='{reminder.Urgency}'><td>{StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency)}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
}
emailBody = emailBody.Replace("{TableBody}", tableBody);
try
{
foreach (string emailAddress in emailAddresses)
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
}
return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) {
string to = emailTo; string to = emailTo;
string from = mailConfig.EmailFrom; string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer; var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to); MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject; message.Subject = emailSubject;
message.Body = emailBody; message.Body = emailBody;
message.IsBodyHtml = isBodyHtml;
SmtpClient client = new SmtpClient(server); SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL; client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port; client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password); client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try try
{
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
}
else
{ {
client.Send(message); client.Send(message);
}
return true; return true;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -14,11 +14,24 @@ namespace CarCareTracker.Helper
if (existingReminder.Metric == ReminderMetric.Both) if (existingReminder.Metric == ReminderMetric.Both)
{ {
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval); existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
{
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval; existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
} }
else
{
existingReminder.Mileage += existingReminder.CustomMileageInterval;
}
}
else if (existingReminder.Metric == ReminderMetric.Odometer) else if (existingReminder.Metric == ReminderMetric.Odometer)
{
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
{ {
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval; existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
} else
{
existingReminder.Mileage += existingReminder.CustomMileageInterval;
}
} }
else if (existingReminder.Metric == ReminderMetric.Date) else if (existingReminder.Metric == ReminderMetric.Date)
{ {

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models; using CarCareTracker.Models;
using System.Globalization;
namespace CarCareTracker.Helper namespace CarCareTracker.Helper
{ {
@@ -10,6 +11,22 @@ namespace CarCareTracker.Helper
public static string DbName = "data/cartracker.db"; public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json"; public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later"; public static string GenericErrorMessage = "An error occurred, please try again later";
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
{
switch (input)
{
case ReminderUrgency.NotUrgent:
return "Not Urgent";
case ReminderUrgency.VeryUrgent:
return "Very Urgent";
case ReminderUrgency.PastDue:
return "Past Due";
default:
return input.ToString();
}
}
public static string TruncateStrings(string input, int maxLength = 25) public static string TruncateStrings(string input, int maxLength = 25)
{ {
@@ -63,5 +80,107 @@ namespace CarCareTracker.Helper
} }
return ""; return "";
} }
public static List<CostForVehicleByMonth> GetBaseLineCosts()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(1), MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(2), MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(3), MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(4), MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(5), MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(6), MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(7), MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(8), MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(9), MonthId = 9, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(10), MonthId = 10, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(11), MonthId = 11, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(12), MonthId = 12, Cost = 0M}
};
}
public static List<CostForVehicleByMonth> GetBaseLineCostsNoMonthName()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth { MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthId = 9, Cost = 0M},
new CostForVehicleByMonth { MonthId = 10, Cost = 0M},
new CostForVehicleByMonth { MonthId = 11, Cost = 0M},
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
};
}
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
{
return new ServiceRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
};
}
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
{
return new CollisionRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
};
}
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
{
return new UpgradeRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes,
Tags = input.Tags
};
}
public static string GetFuelEconomyUnit(bool useKwh, bool useHours, bool useMPG, bool useUKMPG)
{
string fuelEconomyUnit;
if (useKwh)
{
var distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
}
else if (useMPG && useUKMPG)
{
fuelEconomyUnit = useHours ? "h/g" : "mpg";
}
else if (useUKMPG)
{
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
}
else
{
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
}
return fuelEconomyUnit;
}
} }
} }

View File

@@ -0,0 +1,62 @@
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
namespace CarCareTracker.Helper
{
public interface ITranslationHelper
{
string Translate(string userLanguage, string text);
}
public class TranslationHelper : ITranslationHelper
{
private readonly IFileHelper _fileHelper;
private readonly IConfiguration _config;
private IMemoryCache _cache;
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache)
{
_fileHelper = fileHelper;
_config = config;
_cache = memoryCache;
}
public string Translate(string userLanguage, string text)
{
bool create = bool.Parse(_config["LUBELOGGER_TRANSLATOR"] ?? "false");
//transform input text into key.
string translationKey = text.Replace(" ", "_");
var translationFilePath = userLanguage == "en_US" ? _fileHelper.GetFullFilePath($"/defaults/en_US.json") : _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false);
var dictionary = _cache.GetOrCreate<Dictionary<string, string>>($"lang_{userLanguage}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(1);
if (File.Exists(translationFilePath))
{
try
{
var translationFile = File.ReadAllText(translationFilePath);
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
return translationDictionary ?? new Dictionary<string, string>();
} catch (Exception ex)
{
return new Dictionary<string, string>();
}
}
else
{
return new Dictionary<string, string>();
}
});
if (dictionary != null && dictionary.ContainsKey(translationKey))
{
return dictionary[translationKey];
}
else if (create && File.Exists(translationFilePath))
{
//create entry
dictionary.Add(translationKey, text);
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
return text;
}
return text;
}
}
}

View File

@@ -25,6 +25,7 @@ namespace CarCareTracker.MapProfile
Map(m => m.Progress).Name(["progress"]); Map(m => m.Progress).Name(["progress"]);
Map(m => m.Type).Name(["type"]); Map(m => m.Type).Name(["type"]);
Map(m => m.Priority).Name(["priority"]); Map(m => m.Priority).Name(["priority"]);
Map(m => m.Tags).Name(["tags"]);
} }
} }
} }

View File

@@ -157,7 +157,13 @@ namespace CarCareTracker.Middleware
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
if (Request.Path.Value == "/Vehicle/Index" && Request.QueryString.HasValue)
{
Response.Redirect($"/Login/Index?redirectURL={Request.Path.Value}{Request.QueryString.Value}");
} else
{
Response.Redirect("/Login/Index"); Response.Redirect("/Login/Index");
}
return Task.CompletedTask; return Task.CompletedTask;
} }
protected override Task HandleForbiddenAsync(AuthenticationProperties properties) protected override Task HandleForbiddenAsync(AuthenticationProperties properties)

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models namespace CarCareTracker.Models
{ {
public class CollisionRecord public class CollisionRecord: GenericRecord
{ {
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -11,6 +11,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>(); public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; } 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 }; }
} }
} }

View File

@@ -18,5 +18,6 @@
public bool MissedFuelUp { get; set; } = false; public bool MissedFuelUp { get; set; } = false;
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>();
} }
} }

View File

@@ -18,6 +18,7 @@
public bool MissedFuelUp { get; set; } = false; public bool MissedFuelUp { get; set; } = false;
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>();
public GasRecord ToGasRecord() { return new GasRecord { public GasRecord ToGasRecord() { return new GasRecord {
Id = Id, Id = Id,
Cost = Cost, Cost = Cost,
@@ -28,7 +29,8 @@
Files = Files, Files = Files,
IsFillToFull = IsFillToFull, IsFillToFull = IsFillToFull,
MissedFuelUp = MissedFuelUp, MissedFuelUp = MissedFuelUp,
Notes = Notes Notes = Notes,
Tags = Tags
}; } }; }
} }
} }

View File

@@ -3,6 +3,7 @@
public class GasRecordInputContainer public class GasRecordInputContainer
{ {
public bool UseKwh { get; set; } public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public GasRecordInput GasRecord { get; set; } public GasRecordInput GasRecord { get; set; }
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@
public class GasRecordViewModelContainer public class GasRecordViewModelContainer
{ {
public bool UseKwh { get; set; } public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>(); public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
} }
} }

15
Models/GenericRecord.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace CarCareTracker.Models
{
public class GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
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>();
}
}

View File

@@ -23,6 +23,7 @@
public string PartNumber { get; set; } public string PartNumber { get; set; }
public string PartSupplier { get; set; } public string PartSupplier { get; set; }
public string PartQuantity { get; set; } public string PartQuantity { get; set; }
public string Tags { get; set; }
} }
public class SupplyRecordExportModel public class SupplyRecordExportModel
@@ -43,12 +44,14 @@
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; }
} }
public class OdometerRecordExportModel public class OdometerRecordExportModel
{ {
public string Date { get; set; } public string Date { get; set; }
public string Odometer { get; set; } public string Odometer { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; }
} }
public class TaxRecordExportModel public class TaxRecordExportModel
{ {
@@ -56,6 +59,7 @@
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Cost { get; set; } public string Cost { get; set; }
public string Tags { get; set; }
} }
public class GasRecordExportModel public class GasRecordExportModel
{ {
@@ -67,6 +71,7 @@
public string IsFillToFull { get; set; } public string IsFillToFull { get; set; }
public string MissedFuelUp { get; set; } public string MissedFuelUp { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public string Tags { get; set; }
} }
public class ReminderExportModel public class ReminderExportModel
{ {

View File

@@ -6,5 +6,8 @@
public int VehicleId { get; set; } public int VehicleId { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string NoteText { get; set; } public string NoteText { get; set; }
public bool Pinned { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -7,6 +7,7 @@
public DateTime Date { get; set; } public DateTime Date { get; set; }
public int Mileage { get; set; } public int Mileage { get; set; }
public string Notes { get; set; } 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<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -8,6 +8,7 @@
public int Mileage { get; set; } public int Mileage { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files }; } 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 }; }
} }
} }

View File

@@ -9,6 +9,7 @@
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false; public bool IsRecurring { get; set; } = false;
public int CustomMileageInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date; public ReminderMetric Metric { get; set; } = ReminderMetric.Date;

View File

@@ -9,6 +9,7 @@
public string Description { get; set; } public string Description { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false; public bool IsRecurring { get; set; } = false;
public int CustomMileageInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date; public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
@@ -22,6 +23,7 @@
IsRecurring = IsRecurring, IsRecurring = IsRecurring,
ReminderMileageInterval = ReminderMileageInterval, ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval, ReminderMonthInterval = ReminderMonthInterval,
CustomMileageInterval = CustomMileageInterval,
Notes = Notes }; } Notes = Notes }; }
} }
} }

View File

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

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models namespace CarCareTracker.Models
{ {
public class ServiceRecord public class ServiceRecord: GenericRecord
{ {
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -11,6 +11,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>(); public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; } 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 }; }
} }
} }

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class SettingsViewModel
{
public UserConfig UserConfig { get; set; }
public List<string> UILanguages { get; set; }
}
}

View File

@@ -8,6 +8,9 @@
public string Description { get; set; } public string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<string> Tags { get; set; } = new List<string>();
} }
} }

View File

@@ -8,7 +8,21 @@
public string Description { get; set; } public string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public TaxRecord ToTaxRecord() { return new TaxRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Description = Description, Notes = Notes, Files = Files }; } public List<string> Tags { get; set; } = new List<string>();
public TaxRecord ToTaxRecord() { return new TaxRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Description = Description,
Notes = Notes,
IsRecurring = IsRecurring,
RecurringInterval = RecurringInterval,
Files = Files,
Tags = Tags
}; }
} }
} }

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models namespace CarCareTracker.Models
{ {
public class UpgradeRecord public class UpgradeRecord: GenericRecord
{ {
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -11,6 +11,7 @@
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>(); public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; } 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 }; }
} }
} }

View File

@@ -10,10 +10,14 @@
public bool HideZero { get; set; } public bool HideZero { get; set; }
public bool UseUKMPG {get;set;} public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; } public bool UseThreeDecimalGasCost { get; set; }
public bool UseMarkDownOnSavedNotes { get; set; }
public bool EnableAutoReminderRefresh { get; set; } public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; } public bool EnableAutoOdometerInsert { get; set; }
public string PreferredGasUnit { get; set; } = string.Empty;
public string PreferredGasMileageUnit { get; set; } = string.Empty;
public string UserNameHash { get; set; } public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;} public string UserPasswordHash { get; set;}
public string UserLanguage { get; set; } = "en_US";
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() { public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard, ImportMode.Dashboard,
ImportMode.ServiceRecord, ImportMode.ServiceRecord,

View File

@@ -9,5 +9,7 @@
public string Model { get; set; } public string Model { get; set; }
public string LicensePlate { get; set; } public string LicensePlate { get; set; }
public bool IsElectric { get; set; } = false; public bool IsElectric { get; set; } = false;
public bool UseHours { get; set; } = false;
public List<string> Tags { get; set; } = new List<string>();
} }
} }

View File

@@ -5,6 +5,8 @@ using CarCareTracker.Logic;
using CarCareTracker.Middleware; using CarCareTracker.Middleware;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -24,6 +26,7 @@ builder.Services.AddSingleton<IUserAccessDataAccess, UserAccessDataAccess>();
builder.Services.AddSingleton<IUserConfigDataAccess, UserConfigDataAccess>(); builder.Services.AddSingleton<IUserConfigDataAccess, UserConfigDataAccess>();
builder.Services.AddSingleton<ISupplyRecordDataAccess, SupplyRecordDataAccess>(); builder.Services.AddSingleton<ISupplyRecordDataAccess, SupplyRecordDataAccess>();
builder.Services.AddSingleton<IPlanRecordDataAccess, PlanRecordDataAccess>(); builder.Services.AddSingleton<IPlanRecordDataAccess, PlanRecordDataAccess>();
builder.Services.AddSingleton<IPlanRecordTemplateDataAccess, PlanRecordTemplateDataAccess>();
builder.Services.AddSingleton<IOdometerRecordDataAccess, OdometerRecordDataAccess>(); builder.Services.AddSingleton<IOdometerRecordDataAccess, OdometerRecordDataAccess>();
//configure helpers //configure helpers
@@ -33,6 +36,7 @@ builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
builder.Services.AddSingleton<IReportHelper, ReportHelper>(); builder.Services.AddSingleton<IReportHelper, ReportHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>(); builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<IConfigHelper, ConfigHelper>(); builder.Services.AddSingleton<IConfigHelper, ConfigHelper>();
builder.Services.AddSingleton<ITranslationHelper, TranslationHelper>();
//configure logic //configure logic
builder.Services.AddSingleton<ILoginLogic, LoginLogic>(); builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
@@ -54,6 +58,17 @@ builder.Services.AddAuthorization(options =>
{ {
options.DefaultPolicy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("AuthN").RequireAuthenticatedUser().Build(); options.DefaultPolicy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("AuthN").RequireAuthenticatedUser().Build();
}); });
//Configure max file upload size
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB
});
builder.Services.Configure<FormOptions>(options =>
{
options.ValueLengthLimit = int.MaxValue;
options.MultipartBodyLengthLimit = int.MaxValue; // if don't set default value is: 128 MB
options.MultipartHeadersLengthLimit = int.MaxValue;
});
var app = builder.Build(); var app = builder.Build();

View File

@@ -2,7 +2,9 @@
A self-hosted, open-source vehicle service records and maintainence tracker. A self-hosted, open-source vehicle service records and maintainence tracker.
Support this project on Patreon: https://patreon.com/LubeLogger 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)
## Why ## 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 maintainence.
@@ -10,6 +12,11 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
## Screenshots ## Screenshots
<a href="/docs/screenshots.md">Screenshots</a> <a href="/docs/screenshots.md">Screenshots</a>
## 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 ## Dependencies
- Bootstrap - Bootstrap
- LiteDB - LiteDB
@@ -17,12 +24,13 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- SweetAlert2 - SweetAlert2
- CsvHelper - CsvHelper
- Chart.js - Chart.js
- Drawdown
## Docker Setup (GHCR) ## Docker Setup (GHCR)
1. Install Docker 1. Install Docker
2. Run `docker pull ghcr.io/hargata/lubelogger:latest` 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. 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 not using traefik, use docker-compose-notraefik.yml 4. If using traefik, use docker-compose.traefik.yml
5. Run `docker-compose up` 5. Run `docker-compose up`
## Docker Setup (Manual Build) ## Docker Setup (Manual Build)
@@ -31,7 +39,7 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that. 3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
4. Run `docker build -t lubelogger -f Dockerfile .` 4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct. 5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If not using traefik, use docker-compose-notraefik.yml 6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up` 7. Run `docker-compose up`
## Additional Docker Instructions ## Additional Docker Instructions

View File

@@ -241,6 +241,21 @@
</div> </div>
@if (User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/reminders/send</code>
</div>
<div class="col-3">
Send reminder emails out to collaborators based on specified urgency.
</div>
<div class="col-3">
(must be root user)<br />
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
</div>
</div>
<div class="row"> <div class="row">
<div class="col-1"> <div class="col-1">
GET GET

View File

@@ -1,10 +1,13 @@
@{ @using CarCareTracker.Helper
@{
ViewData["Title"] = "Admin"; ViewData["Title"] = "Admin";
} }
@inject IConfiguration config; @inject IConfiguration config;
@inject ITranslationHelper translator
@{ @{
bool emailServerIsSetup = true; bool emailServerIsSetup = true;
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>(); var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
var userLanguage = config[nameof(UserConfig.UserLanguage)] ?? "en_US";
if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer)) if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{ {
emailServerIsSetup = false; emailServerIsSetup = false;
@@ -17,31 +20,31 @@
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a> <a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a>
</div> </div>
<div class="col-11"> <div class="col-11">
<span class="display-6">Admin Panel</span> <span class="display-6">@translator.Translate(userLanguage, "Admin Panel")</span>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="row"> <div class="row">
<div class="col-md-5 col-12"> <div class="col-md-5 col-12">
<span class="lead">Tokens</span> <span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
<hr /> <hr />
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Generate User Token</button> <button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate User Token")</button>
</div> </div>
<div class="col-6 d-flex align-items-center"> <div class="col-6 d-flex align-items-center">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")> <input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">Auto Notify(via Email)</label> <label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Auto Notify(via Email)")</label>
</div> </div>
</div> </div>
</div> </div>
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-4">Token</th> <th scope="col" class="col-4">@translator.Translate(userLanguage, "Token")</th>
<th scope="col" class="col-6">Issued To</th> <th scope="col" class="col-6">@translator.Translate(userLanguage, "Issued To")</th>
<th scope="col" class="col-2">Delete</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -59,15 +62,15 @@
</table> </table>
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<span class="lead">Users</span> <span class="lead">@translator.Translate(userLanguage, "Users")</span>
<hr /> <hr />
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-4">Username</th> <th scope="col" class="col-4">@translator.Translate(userLanguage, "Username")</th>
<th scope="col" class="col-4">Email</th> <th scope="col" class="col-4">@translator.Translate(userLanguage, "Email")</th>
<th scope="col" class="col-2">Is Admin</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Is Admin")</th>
<th scope="col" class="col-2">Delete</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -92,7 +95,7 @@
if (data){ if (data){
reloadPage(); reloadPage();
} else { } else {
errorToast("An error has occurred, please try again later."); errorToast(genericErrorMessage());
} }
}); });
} }
@@ -104,7 +107,7 @@
if (data) { if (data) {
reloadPage(); reloadPage();
} else { } else {
errorToast("An error has occurred, please try again later."); errorToast(genericErrorMessage());
} }
}); });
} }
@@ -113,7 +116,7 @@
if (data) { if (data) {
reloadPage(); reloadPage();
} else { } else {
errorToast("An error has occurred, please try again later."); errorToast(genericErrorMessage());
} }
}) })
} }

View File

@@ -1,33 +1,37 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableAuth = config.GetUserConfig(User).EnableAuth; var userConfig = config.GetUserConfig(User);
var enableAuth = userConfig.EnableAuth;
var userLanguage = userConfig.UserLanguage;
var logoUrl = config.GetLogoUrl();
} }
@model string @model string
@{ @{
ViewData["Title"] = "LubeLogger"; ViewData["Title"] = "LubeLogger";
} }
@section Scripts { @section Scripts {
<script src="~/js/garage.js" asp-append-version="true"></script> <script src="~/js/garage.js"></script>
} }
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()"> <div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="navbar-nav" id="homeTab" role="tablist"> <ul class="navbar-nav" id="homeTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>Garage</span></button> <button class="nav-link user-select-none @(Model == "garage" ? "active" : "")" ontouchstart="detectLongTouch(this)" ontouchend="detectTouchEndPremature(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>@translator.Translate(userLanguage,"Garage")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button> <button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</span></button>
</li> </li>
@if (User.IsInRole("CookieAuth")) @if (User.IsInRole("CookieAuth"))
{ {
@if (User.IsInRole(nameof(UserData.IsAdmin))) @if (User.IsInRole(nameof(UserData.IsAdmin)))
{ {
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>Admin Panel</span></a> <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> </li>
} }
<li class="nav-item" role="presentation"> <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>Logout</span></button> <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> </li>
} }
</ul> </ul>
@@ -35,7 +39,7 @@
<div class="container"> <div class="container">
<div class="row mt-2"> <div class="row mt-2">
<div class="d-flex lubelogger-navbar"> <div class="d-flex lubelogger-navbar">
<img src="/defaults/lubelogger_logo.png" /> <img src="@logoUrl" />
<div class="lubelogger-navbar-button"> <div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button> <button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div> </div>
@@ -44,10 +48,10 @@
<hr /> <hr />
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist"> <ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>Garage</button> <button class="nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>@translator.Translate(userLanguage,"Garage")</button>
</li> </li>
<li class="nav-item ms-auto" role="presentation"> <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>Settings</button> <button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</button>
</li> </li>
@if (User.IsInRole("CookieAuth")) @if (User.IsInRole("CookieAuth"))
{ {
@@ -57,11 +61,11 @@
@if (User.IsInRole(nameof(UserData.IsAdmin))) @if (User.IsInRole(nameof(UserData.IsAdmin)))
{ {
<li> <li>
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>Admin Panel</a> <a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</a>
</li> </li>
} }
<li> <li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button> <button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage,"Logout")</button>
</li> </li>
</ul> </ul>
</li> </li>
@@ -69,9 +73,8 @@
</ul> </ul>
<div class="tab-content" id="homeTab"> <div class="tab-content" id="homeTab">
<div class="tab-pane fade @(Model == "garage" ? "show active" : "")" id="garage-tab-pane" role="tabpanel" tabindex="0"> <div class="tab-pane fade @(Model == "garage" ? "show active" : "")" id="garage-tab-pane" role="tabpanel" tabindex="0">
<div class="row"> <div id="garageContainer">
<div id="garageContainer" class="row gy-3 align-items-stretch">
</div>
</div> </div>
</div> </div>
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0"> <div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">

View File

@@ -1,14 +1,33 @@
@model List<Vehicle> @model List<Vehicle>
@{
@if (Model.Any()) var recordTags = Model.SelectMany(x => x.Tags).Distinct();
}
@if (recordTags.Any())
{ {
foreach (Vehicle vehicle in Model) <div class='row'>
<div class="d-flex align-items-center flex-wrap mt-4">
@foreach (string recordTag in recordTags)
{ {
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4"> <span onclick="filterGarage(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>
}
<div class="row">
<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)"> <div class="card" onclick="viewVehicle(@vehicle.Id)">
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" /> <img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-truncate">@($"{vehicle.Year}")</h5> <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.Make}")</h5>
<h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5> <h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5>
<p class="card-text text-truncate">@vehicle.LicensePlate</p> <p class="card-text text-truncate">@vehicle.LicensePlate</p>
@@ -16,9 +35,10 @@
</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="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4">
<div class="card" onclick="showAddVehicleModal()" style="height:100%;"> <div class="card" onclick="showAddVehicleModal()" style="height:100%;">
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" /> <img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
</div> </div>
</div> </div>
</div>
</div>

View File

@@ -1,152 +1,185 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@model UserConfig @model SettingsViewModel
@inject ITranslationHelper translator
@{
var userLanguage = Model.UserConfig.UserLanguage;
}
<div class="row"> <div class="row">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<h6 class="display-6 mt-2">Settings</h6> <h6 class="display-6 mt-2">@translator.Translate(userLanguage, "Settings")</h6>
</div> </div>
<hr /> <hr />
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<input id="preferredGasUnit" style="display:none;" value="@Model.UserConfig.PreferredGasUnit" />
<input id="preferredFuelMileageUnit" style="display:none;" value="@Model.UserConfig.PreferredGasMileageUnit" />
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UseDarkMode"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
<label class="form-check-label" for="enableDarkMode">Dark Mode</label> <label class="form-check-label" for="enableDarkMode">@translator.Translate(userLanguage, "Dark Mode")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableCsvImports" checked="@Model.EnableCsvImports"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableCsvImports" checked="@Model.UserConfig.EnableCsvImports">
<label class="form-check-label" for="enableCsvImports">Enable CSV Imports</label> <label class="form-check-label" for="enableCsvImports">@translator.Translate(userLanguage, "Enable CSV Imports")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMPG" checked="@Model.UseMPG"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMPG" checked="@Model.UserConfig.UseMPG">
<label class="form-check-label" for="useMPG">Use Imperial Calculation for Fuel Economy Calculations(MPG)<br /><small class="text-body-secondary">This Will Also Change Units to Miles and Gallons</small></label> <label class="form-check-label" for="useMPG">@translator.Translate(userLanguage, "Use Imperial Calculation for Fuel Economy Calculations(MPG)")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "This Will Also Change Units to Miles and Gallons")</small></label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UseUKMPG"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UserConfig.UseUKMPG">
<label class="form-check-label" for="useUKMPG">Use UK MPG Calculation<br /><small class="text-body-secondary">Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation</small></label> <label class="form-check-label" for="useUKMPG">@translator.Translate(userLanguage, "Use UK MPG Calculation")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation")</small></label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UserConfig.UseDescending">
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label> <label class="form-check-label" for="useDescending">@translator.Translate(userLanguage, "Sort lists in Descending Order(Newest to Oldest)")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.HideZero"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.UserConfig.HideZero">
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label> <label class="form-check-label" for="hideZero">@translator.Translate(userLanguage, "Replace $0.00 Costs with ---")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UseThreeDecimalGasCost"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UserConfig.UseThreeDecimalGasCost">
<label class="form-check-label" for="useThreeDecimal">Use Three Decimals For Fuel Cost</label> <label class="form-check-label" for="useThreeDecimal">@translator.Translate(userLanguage, "Use Three Decimals For Fuel Cost")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoReminderRefresh" checked="@Model.EnableAutoReminderRefresh"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMarkDownOnSavedNotes" checked="@Model.UserConfig.UseMarkDownOnSavedNotes">
<label class="form-check-label" for="enableAutoReminderRefresh">Auto Refresh Lapsed Recurring Reminders</label> <label class="form-check-label" for="useMarkDownOnSavedNotes">@translator.Translate(userLanguage, "Display Saved Notes in Markdown")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoOdometerInsert" checked="@Model.EnableAutoOdometerInsert"> <input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoReminderRefresh" checked="@Model.UserConfig.EnableAutoReminderRefresh">
<label class="form-check-label" for="enableAutoOdometerInsert">Auto Insert Odometer Records<br /><small class="text-body-secondary">Only when Adding Service/Repair/Upgrade/Fuel Record or Completing a Plan</small></label> <label class="form-check-label" for="enableAutoReminderRefresh">@translator.Translate(userLanguage, "Auto Refresh Lapsed Recurring Reminders")</label>
</div>
<div class="form-check form-switch">
<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>
@if (User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth"> <input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.UserConfig.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label> <label class="form-check-label" for="enableAuth">@translator.Translate(userLanguage, "Enable Authentication")</label>
</div> </div>
} }
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="row" id="visibleTabs"> <div class="row" id="visibleTabs">
<div class="col-12"> <div class="col-12">
<span class="lead">Visible Tabs</span> <span class="lead">@translator.Translate(userLanguage, "Visible Tabs")</span>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ServiceRecord" id="serviceRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ServiceRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ServiceRecord" id="serviceRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.ServiceRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="serviceRecordTab">Service Records</label> <label class="form-check-label stretched-link" for="serviceRecordTab">@translator.Translate(userLanguage, "Service Records")</label>
</li> </li>
<li class="list-group-item d-none"> <li class="list-group-item d-none">
<input onChange="updateSettings()" disabled class="form-check-input me-1" type="checkbox" value="Dashboard" id="dashboardTab" @(Model.VisibleTabs.Contains(ImportMode.Dashboard) ? "checked" : "")> <input onChange="updateSettings()" disabled class="form-check-input me-1" type="checkbox" value="Dashboard" id="dashboardTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.Dashboard) ? "checked" : "")>
<label class="form-check-label stretched-link" for="dashboardTab">Dashboard</label> <label class="form-check-label stretched-link" for="dashboardTab">@translator.Translate(userLanguage, "Dashboard")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="RepairRecord" id="repairRecordTab" @(Model.VisibleTabs.Contains(ImportMode.RepairRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="RepairRecord" id="repairRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.RepairRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="repairRecordTab">Repairs</label> <label class="form-check-label stretched-link" for="repairRecordTab">@translator.Translate(userLanguage, "Repairs")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="UpgradeRecord" id="upgradeRecordTab" @(Model.VisibleTabs.Contains(ImportMode.UpgradeRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="UpgradeRecord" id="upgradeRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.UpgradeRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="upgradeRecordTab">Upgrades</label> <label class="form-check-label stretched-link" for="upgradeRecordTab">@translator.Translate(userLanguage, "Upgrades")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="GasRecord" id="gasRecordTab" @(Model.VisibleTabs.Contains(ImportMode.GasRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="GasRecord" id="gasRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.GasRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="gasRecordTab">Fuel</label> <label class="form-check-label stretched-link" for="gasRecordTab">@translator.Translate(userLanguage, "Fuel")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="OdometerRecord" id="odometerRecordTab" @(Model.VisibleTabs.Contains(ImportMode.OdometerRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="OdometerRecord" id="odometerRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.OdometerRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="odometerRecordTab">Odometer</label> <label class="form-check-label stretched-link" for="odometerRecordTab">@translator.Translate(userLanguage, "Odometer")</label>
</li> </li>
</ul> </ul>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="TaxRecord" id="taxRecordTab" @(Model.VisibleTabs.Contains(ImportMode.TaxRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="TaxRecord" id="taxRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.TaxRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="taxRecordTab">Taxes</label> <label class="form-check-label stretched-link" for="taxRecordTab">@translator.Translate(userLanguage, "Taxes")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="NoteRecord" id="noteRecordTab" @(Model.VisibleTabs.Contains(ImportMode.NoteRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="NoteRecord" id="noteRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.NoteRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="noteRecordTab">Notes</label> <label class="form-check-label stretched-link" for="noteRecordTab">@translator.Translate(userLanguage, "Notes")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ReminderRecord" id="reminderRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ReminderRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ReminderRecord" id="reminderRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.ReminderRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="reminderRecordTab">Reminder</label> <label class="form-check-label stretched-link" for="reminderRecordTab">@translator.Translate(userLanguage, "Reminder")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="SupplyRecord" id="supplyRecordTab" @(Model.VisibleTabs.Contains(ImportMode.SupplyRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="SupplyRecord" id="supplyRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.SupplyRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="supplyRecordTab">Supplies</label> <label class="form-check-label stretched-link" for="supplyRecordTab">@translator.Translate(userLanguage, "Supplies")</label>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="PlanRecord" id="planRecordTab" @(Model.VisibleTabs.Contains(ImportMode.PlanRecord) ? "checked" : "")> <input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="PlanRecord" id="planRecordTab" @(Model.UserConfig.VisibleTabs.Contains(ImportMode.PlanRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="planRecordTab">Planner</label> <label class="form-check-label stretched-link" for="planRecordTab">@translator.Translate(userLanguage, "Planner")</label>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<span class="lead">Default Tab</span> <span class="lead">@translator.Translate(userLanguage, "Default Tab")</span>
<select class="form-select" onchange="updateSettings()" id="defaultTab"> <select class="form-select" onchange="updateSettings()" id="defaultTab">
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.Dashboard)) value="Dashboard">Dashboard</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.Dashboard)) value="Dashboard">@translator.Translate(userLanguage, "Dashboard")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model,ImportMode.ServiceRecord)) value="ServiceRecord">Service Record</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.ServiceRecord)) value="ServiceRecord">@translator.Translate(userLanguage, "Service Record")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.RepairRecord)) value="RepairRecord">Repairs</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.RepairRecord)) value="RepairRecord">@translator.Translate(userLanguage, "Repairs")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.UpgradeRecord)) value="UpgradeRecord">Upgrades</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.UpgradeRecord)) value="UpgradeRecord">@translator.Translate(userLanguage, "Upgrades")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.GasRecord)) value="GasRecord">Fuel</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.GasRecord)) value="GasRecord">@translator.Translate(userLanguage, "Fuel")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.TaxRecord)) value="TaxRecord">Tax</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.TaxRecord)) value="TaxRecord">@translator.Translate(userLanguage, "Tax")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.NoteRecord)) value="NoteRecord">Notes</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.NoteRecord)) value="NoteRecord">@translator.Translate(userLanguage, "Notes")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.ReminderRecord)) value="ReminderRecord">Reminders</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.ReminderRecord)) value="ReminderRecord">@translator.Translate(userLanguage, "Reminders")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.SupplyRecord)) value="SupplyRecord">Supplies</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.SupplyRecord)) value="SupplyRecord">@translator.Translate(userLanguage, "Supplies")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.PlanRecord)) value="PlanRecord">Planner</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.PlanRecord)) value="PlanRecord">@translator.Translate(userLanguage, "Planner")</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.OdometerRecord)) value="OdometerRecord">Odometer</!option> <!option @(StaticHelper.DefaultTabSelected(Model.UserConfig, ImportMode.OdometerRecord)) value="OdometerRecord">@translator.Translate(userLanguage, "Odometer")</!option>
</select> </select>
</div> </div>
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
}
</select>
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser))) @if (User.IsInRole(nameof(UserData.IsRootUser)))
{ {
<div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<span class="lead">Backups</span> <span class="lead">@translator.Translate(userLanguage, "Backups")</span>
<div class="row"> <div class="row">
<div class="col-6 d-grid"> <div class="col-6 d-grid">
<button onclick="makeBackup()" class="btn btn-primary btn-md">Make</button> <button onclick="makeBackup()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Make")</button>
</div> </div>
<div class="col-6 d-grid"> <div class="col-6 d-grid">
<input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup"> <input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup">
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">Restore</button> <button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">@translator.Translate(userLanguage, "Restore")</button>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Manage Languages")</span>
<div class="row">
<div class="col-6 d-grid">
<input onChange="uploadLanguage(this)" type="file" accept=".json" class="d-none" id="inputLanguage">
<button onclick="openUploadLanguage()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Upload")</button>
</div>
<div class="col-6 d-grid">
<button onclick="deleteLanguage()" @(Model.UserConfig.UserLanguage == "en_US" ? "disabled" : "") class="btn btn-danger btn-md">@translator.Translate(userLanguage, "Delete")</button>
</div>
</div> </div>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
</div>
<div class="row"> <div class="row">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<h6 class="display-6 mt-2">About</h6> <h6 class="display-6 mt-2">@translator.Translate(userLanguage, "About")</h6>
</div> </div>
<hr /> <hr />
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@@ -154,14 +187,14 @@
<img src="/defaults/lubelogger_logo.png" /> <img src="/defaults/lubelogger_logo.png" />
</div> </div>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<small class="text-body-secondary">Version 1.0.7</small> <small class="text-body-secondary">Version 1.1.4</small>
</div> </div>
<p class="lead"> <p class="lead">
Proudly developed in the rural town of Price, Utah by Hargata Softworks. Proudly developed in the rural town of Price, Utah by Hargata Softworks.
</p> </p>
<p class="lead"> <p class="lead">
If you enjoyed using this app, please consider spreading the good word.<br /> If you enjoyed using this app, please consider spreading the good word.<br />
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
</p> </p>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Hometown Shoutout</h6> <h6 class="display-7 mt-2">Hometown Shoutout</h6>
@@ -187,6 +220,7 @@
<li class="list-group-item">SweetAlert2</li> <li class="list-group-item">SweetAlert2</li>
<li class="list-group-item">CsvHelper</li> <li class="list-group-item">CsvHelper</li>
<li class="list-group-item">Chart.js</li> <li class="list-group-item">Chart.js</li>
<li class="list-group-item">Drawdown</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -197,6 +231,14 @@
}); });
return visibleTabs.toArray(); return visibleTabs.toArray();
} }
function deleteLanguage() {
var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`;
$.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) {
//reset user language back to en_US
$("#defaultLanguage").val('en_US');
updateSettings();
});
}
function updateSettings() { function updateSettings() {
var visibleTabs = getCheckedTabs(); var visibleTabs = getCheckedTabs();
var defaultTab = $("#defaultTab").val(); var defaultTab = $("#defaultTab").val();
@@ -211,8 +253,12 @@
hideZero: $("#hideZero").is(":checked"), hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked"), useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"), useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"), enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"), enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
userLanguage: $("#defaultLanguage").val(),
visibleTabs: visibleTabs, visibleTabs: visibleTabs,
defaultTab: defaultTab defaultTab: defaultTab
} }
@@ -222,7 +268,7 @@
if (data) { if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500); setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else { } else {
errorToast("An error occurred, please try again later.") errorToast(genericErrorMessage());
} }
}) })
} }
@@ -231,9 +277,37 @@
window.location.href = data; window.location.href = data;
}); });
} }
function openUploadLanguage(){
$("#inputLanguage").click();
}
function openRestoreBackup() { function openRestoreBackup() {
$("#inputBackup").click(); $("#inputBackup").click();
} }
function uploadLanguage(event){
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
$.ajax({
url: "/Files/HandleTranslationFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
sloader.hide();
if (response.success) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(response.message);
}
},
error: function(){
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function restoreBackup(event) { function restoreBackup(event) {
let formData = new FormData(); let formData = new FormData();
formData.append("file", event.files[0]); formData.append("file", event.files[0]);
@@ -253,10 +327,14 @@
successToast("Backup Restored"); successToast("Backup Restored");
setTimeout(function () { window.location.href = '/Home/Index' }, 500); setTimeout(function () { window.location.href = '/Home/Index' }, 500);
} else { } else {
errorToast("An error occurred, please try again later."); errorToast(genericErrorMessage());
} }
}); });
} }
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
} }
}); });
} }
@@ -286,7 +364,7 @@
if (data) { if (data) {
setTimeout(function () { window.location.href = '/Login' }, 500); setTimeout(function () { window.location.href = '/Login' }, 500);
} else { } else {
errorToast("An error occurred, please try again later."); errorToast(genericErrorMessage());
} }
}) })
} }
@@ -296,7 +374,7 @@
if (data) { if (data) {
setTimeout(function () { window.location.href = '/Home' }, 1000); setTimeout(function () { window.location.href = '/Home' }, 1000);
} else { } else {
errorToast("An error occurred, please try again later."); errorToast(genericErrorMessage());
} }
}); });
} }

View File

@@ -1,25 +1,32 @@
@{ @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Login"; ViewData["Title"] = "LubeLogger - Login";
} }
@section Scripts { @section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script> <script src="~/js/login.js"></script>
} }
<div class="container d-flex align-items-center justify-content-center" style="height:100vh"> <div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<img src="/defaults/lubelogger_logo.png" /> <img src="@logoUrl" />
<div class="form-group"> <div class="form-group">
<label for="inputUserName">Username</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control"> <input type="text" id="inputUserName" class="form-control">
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Request</button> <button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Request")</button>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/ResetPassword" class="btn btn-link mt-2">I Have a Token</a> <a href="/Login/ResetPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "I Have a Token")</a>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a> <a href="/Login/Index" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Back to Login")</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,34 +1,47 @@
@{ @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model string
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Login"; ViewData["Title"] = "LubeLogger - Login";
} }
@section Scripts { @section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script> <script src="~/js/login.js"></script>
} }
<div class="container d-flex align-items-center justify-content-center" style="height:100vh"> <div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<img src="/defaults/lubelogger_logo.png" /> <img src="@logoUrl" />
<div class="form-group"> <div class="form-group">
<label for="inputUserName">Username</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control"> <input type="text" id="inputUserName" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">Password</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control"> <input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputPersistent"> <input class="form-check-input" type="checkbox" role="switch" id="inputPersistent">
<label class="form-check-label" for="inputPersistent">Remember Me</label> <label class="form-check-label" for="inputPersistent">@translator.Translate(userLanguage, "Remember Me")</label>
</div> </div>
<div class="d-grid"> <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>Login</button> <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> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">Forgot Password</a> <a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">Register</a> <a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
function getRedirectURL(){
return { url: decodeHTMLEntities('@Model') };
}
</script>

View File

@@ -1,34 +1,41 @@
@{ @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Register"; ViewData["Title"] = "LubeLogger - Register";
} }
@section Scripts { @section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script> <script src="~/js/login.js"></script>
} }
<div class="container d-flex align-items-center justify-content-center" style="height:100vh"> <div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<img src="/defaults/lubelogger_logo.png" /> <img src="@logoUrl" />
<div class="form-group"> <div class="form-group">
<label for="inputToken">Token</label> <label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control"> <input type="text" id="inputToken" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">Email Address</label> <label for="inputUserName">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control"> <input type="text" id="inputEmail" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">Username</label> <label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
<input type="text" id="inputUserName" class="form-control"> <input type="text" id="inputUserName" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">Password</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
<input type="password" id="inputUserPassword" class="form-control"> <input type="password" id="inputUserPassword" class="form-control">
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>Register</button> <button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a> <a href="/Login/Index" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Back to Login")</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,30 +1,37 @@
@{ @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
}
@{
ViewData["Title"] = "LubeLogger - Register"; ViewData["Title"] = "LubeLogger - Register";
} }
@section Scripts { @section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script> <script src="~/js/login.js"></script>
} }
<div class="container d-flex align-items-center justify-content-center" style="height:100vh"> <div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<img src="/defaults/lubelogger_logo.png" /> <img src="@logoUrl" />
<div class="form-group"> <div class="form-group">
<label for="inputToken">Token</label> <label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control"> <input type="text" id="inputToken" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserName">Email Address</label> <label for="inputUserName">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control"> <input type="text" id="inputEmail" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputUserPassword">New Password</label> <label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label>
<input type="password" id="inputUserPassword" class="form-control"> <input type="password" id="inputUserPassword" class="form-control">
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Reset Password</button> <button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Reset Password")</button>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a> <a href="/Login/Index" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Back to Login")</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1 +1,8 @@
<h1>Access Denied</h1> <div class="row">
<div class="col-1">
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a>
</div>
<div class="col-11">
<h1>Access Denied</h1>
</div>
</div>

View File

@@ -1,12 +1,17 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
<!DOCTYPE html> <!DOCTYPE html>
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var userConfig = config.GetUserConfig(User); var userConfig = config.GetUserConfig(User);
var useDarkMode = userConfig.UseDarkMode; var useDarkMode = userConfig.UseDarkMode;
var enableCsvImports = userConfig.EnableCsvImports; var enableCsvImports = userConfig.EnableCsvImports;
var useMPG = userConfig.UseMPG;
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat; var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
var userLanguage = userConfig.UserLanguage;
shortDatePattern = shortDatePattern.ToLower(); shortDatePattern = shortDatePattern.ToLower();
if (!shortDatePattern.Contains("dd")) if (!shortDatePattern.Contains("dd"))
{ {
@@ -28,9 +33,10 @@
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/lib/bootstrap-tagsinput/bootstrap-tagsinput.css" />
<link rel="stylesheet" href="~/css/loader.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css"/>
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/loader.css"/>
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css"/>
<link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" /> <link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" />
<link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" /> <link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" /> <link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
@@ -41,13 +47,18 @@
<script src="~/js/shared.js"></script> <script src="~/js/shared.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/bootstrap-datepicker/js/bootstrap-datepicker.min.js"></script> <script src="~/lib/bootstrap-datepicker/js/bootstrap-datepicker.min.js"></script>
<script src="~/lib/bootstrap-tagsinput/bootstrap-tagsinput.js"></script>
<script src="~/sweetalert/sweetalert2.all.min.js"></script> <script src="~/sweetalert/sweetalert2.all.min.js"></script>
<script src="~/js/loader.js"></script> <script src="~/js/loader.js"></script>
<script> <script>
function getGlobalConfig() { function getGlobalConfig() {
return { return {
useDarkMode : "@useDarkMode" == "True", useDarkMode : "@useDarkMode" == "True",
enableCsvImport : "@enableCsvImports" == "True" enableCsvImport : "@enableCsvImports" == "True",
useMarkDown: "@useMarkDown" == "True",
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
useThreeDecimals: "@useThreeDecimals" == "True",
useMPG: "@useMPG" == "True"
} }
} }
function getShortDatePattern() { function getShortDatePattern() {
@@ -59,10 +70,16 @@
//remove thousands separator. //remove thousands separator.
var thousandSeparator = "@numberFormat.NumberGroupSeparator"; var thousandSeparator = "@numberFormat.NumberGroupSeparator";
var decimalSeparator = "@numberFormat.NumberDecimalSeparator"; var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
if (input == "---") {
input = "0";
}
//strip thousands from input. //strip thousands from input.
input = input.replace(thousandSeparator, ""); input = input.replace(thousandSeparator, "");
//convert to JS format where decimal is only separated by . //convert to JS format where decimal is only separated by .
input = input.replace(decimalSeparator, "."); input = input.replace(decimalSeparator, ".");
//remove any currency symbol
input = input.replace(currencySymbol, "");
return parseFloat(input); return parseFloat(input);
} }
function globalFloatToString(input) { function globalFloatToString(input) {
@@ -70,6 +87,9 @@
input = input.replace(".", decimalSeparator); input = input.replace(".", decimalSeparator);
return input; return input;
} }
function genericErrorMessage(){
return decodeHTMLEntities('@translator.Translate(userLanguage, "An error has occurred, please try again later")');
}
</script> </script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</head> </head>

View File

@@ -3,68 +3,71 @@
ViewData["Title"] = "LubeLogger - View Vehicle"; ViewData["Title"] = "LubeLogger - View Vehicle";
} }
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var userConfig = config.GetUserConfig(User); var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
@model Vehicle @model Vehicle
@section Scripts { @section Scripts {
<script src="~/js/vehicle.js" asp-append-version="true"></script> <script src="~/js/vehicle.js"></script>
<script src="~/js/servicerecord.js" asp-append-version="true"></script> <script src="~/js/servicerecord.js"></script>
<script src="~/js/gasrecord.js" asp-append-version="true"></script> <script src="~/js/gasrecord.js"></script>
<script src="~/js/collisionrecord.js" asp-append-version="true"></script> <script src="~/js/collisionrecord.js"></script>
<script src="~/js/taxrecord.js" asp-append-version="true"></script> <script src="~/js/taxrecord.js"></script>
<script src="~/js/reminderrecord.js" asp-append-version="true"></script> <script src="~/js/reminderrecord.js"></script>
<script src="~/js/upgraderecord.js" asp-append-version="true"></script> <script src="~/js/upgraderecord.js"></script>
<script src="~/js/note.js" asp-append-version="true"></script> <script src="~/js/note.js"></script>
<script src="~/js/reports.js" asp-append-version="true"></script> <script src="~/js/reports.js"></script>
<script src="~/js/supplyrecord.js" asp-append-version="true"></script> <script src="~/js/supplyrecord.js"></script>
<script src="~/js/planrecord.js" asp-append-version="true"></script> <script src="~/js/planrecord.js"></script>
<script src="~/js/odometerrecord.js" asp-append-version="true"></script> <script src="~/js/odometerrecord.js"></script>
<script src="~/lib/chart-js/chart.umd.js"></script> <script src="~/lib/chart-js/chart.umd.js"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
} }
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()"> <div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="nav navbar-nav" id="vehicleTab" role="tablist"> <ul class="nav navbar-nav" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" onclick="returnToGarage()"><span class="display-3 ms-2"><i class="bi bi-arrow-left-square"></i>Garage</span></button> <button class="nav-link" onclick="returnToGarage()"><span class="display-3 ms-2"><i class="bi bi-arrow-left-square me-2"></i>@translator.Translate(userLanguage,"Garage")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" onclick="editVehicle(@Model.Id)"><span class="display-3 ms-2"><i class="bi bi-pencil-square"></i>Edit Vehicle</span></button> <button class="nav-link" onclick="editVehicle(@Model.Id)"><span class="display-3 ms-2"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Edit Vehicle")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>Dashboard</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>@translator.Translate(userLanguage, "Dashboard")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-bar-chart-steps me-2"></i>Planner</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-bar-chart-steps me-2"></i>@translator.Translate(userLanguage, "Planner")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-speedometer me-2"></i>Odometer</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-speedometer me-2"></i>@translator.Translate(userLanguage, "Odometer")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>Service Records</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>@translator.Translate(userLanguage, "Service Records")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-exclamation-octagon me-2"></i>@translator.Translate(userLanguage,"Repairs")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-wrench-adjustable me-2"></i>@translator.Translate(userLanguage, "Upgrades")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-fuel-pump me-2"></i>Fuel</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Fuel")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-shop me-2"></i>Supplies</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-currency-dollar me-2"></i>Taxes</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-currency-dollar me-2"></i>@translator.Translate(userLanguage, "Taxes")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-journal-bookmark me-2"></i>Notes</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-journal-bookmark me-2"></i>@translator.Translate(userLanguage, "Notes")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</span></button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>@translator.Translate(userLanguage, "Reminders")</span></button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><span class="display-3 ms-2"><i class="bi bi-trash me-2"></i>Delete Vehicle</span></button> <button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><span class="display-3 ms-2"><i class="bi bi-trash me-2"></i>@translator.Translate(userLanguage, "Delete Vehicle")</span></button>
</li> </li>
</ul> </ul>
</div> </div>
@@ -82,42 +85,42 @@
<hr /> <hr />
<ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist"> <ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Dash</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>@translator.Translate(userLanguage,"Dashboard")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps me-2"></i>Planner</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps me-2"></i>@translator.Translate(userLanguage, "Planner")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer me-2"></i>Odometer</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer me-2"></i>@translator.Translate(userLanguage, "Odometer")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>Service Records</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>@translator.Translate(userLanguage, "Service Records")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>@translator.Translate(userLanguage, "Repairs")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>@translator.Translate(userLanguage, "Upgrades")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Fuel")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>Supplies</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>Taxes</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>@translator.Translate(userLanguage, "Taxes")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>@translator.Translate(userLanguage, "Notes")</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</button> <button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>@translator.Translate(userLanguage, "Reminders")</button>
</li> </li>
<li class="nav-item dropdown ms-auto" role="presentation"> <li class="nav-item dropdown ms-auto" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Manage Vehicle</a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">@translator.Translate(userLanguage, "Manage Vehicle")</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><i class="bi bi-trash me-2"></i>Delete Vehicle</button></li> <li><button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><i class="bi bi-trash me-2"></i>@translator.Translate(userLanguage, "Delete Vehicle")</button></li>
</ul> </ul>
</li> </li>
</ul> </ul>
@@ -147,7 +150,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="reminderRecordModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" data-bs-focus="false" id="reminderRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="reminderRecordModalContent"> <div class="modal-content" id="reminderRecordModalContent">
</div> </div>

View File

@@ -1,6 +1,13 @@
@model ImportMode @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model ImportMode
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Import Data from CSV</h5> <h5 class="modal-title">@translator.Translate(userLanguage, "Import Data from CSV")</h5>
<button type="button" class="btn-close" onclick="hideBulkImportModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideBulkImportModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -9,37 +16,40 @@
<div class="form-group"> <div class="form-group">
<div class="col-12"> <div class="col-12">
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
In order for this utility to function properly, your CSV file MUST be formatted exactly like the provided sample. @translator.Translate(userLanguage, "In order for this utility to function properly, your CSV file MUST be formatted exactly like the provided sample. Dates must be supplied in a string. Numbers must be supplied as numbers without currency formatting.")
Dates must be supplied in a string.
Numbers must be supplied as numbers without currency formatting.
</div> </div>
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding. @translator.Translate(userLanguage, "Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.")
</div> </div>
@if (Model == ImportMode.GasRecord) @if (Model == ImportMode.GasRecord)
{ {
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord) }
else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
{ {
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} else if (Model == ImportMode.TaxRecord) }
else if (Model == ImportMode.TaxRecord)
{ {
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} else if (Model == ImportMode.SupplyRecord) }
else if (Model == ImportMode.SupplyRecord)
{ {
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} else if (Model == ImportMode.PlanRecord) }
else if (Model == ImportMode.PlanRecord)
{ {
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} else if (Model == ImportMode.OdometerRecord) }
else if (Model == ImportMode.OdometerRecord)
{ {
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">Download Sample</a> <a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
} }
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12"> <div class="col-12">
<label for="csvFileUploader">Upload CSV File</label> <label for="csvFileUploader">@translator.Translate(userLanguage, "Upload CSV File")</label>
<input onChange="uploadFileAsync(this)" type="file" multiple accept=".csv" class="form-control-file" id="csvFileUploader"> <input onChange="uploadFileAsync(this)" type="file" multiple accept=".csv" class="form-control-file" id="csvFileUploader">
</div> </div>
</div> </div>
@@ -47,8 +57,8 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideBulkImportModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideBulkImportModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="importFromCsv()" class="btn btn-primary">Import</button> <button type="button" onclick="importFromCsv()" class="btn btn-primary">@translator.Translate(userLanguage, "Import")</button>
</div> </div>
<script> <script>
var uploadedFile = ""; var uploadedFile = "";
@@ -79,7 +89,7 @@
getVehicleOdometerRecords(vehicleId); getVehicleOdometerRecords(vehicleId);
} }
} else { } else {
errorToast("An error has occurred, please double check the data and try again."); errorToast(genericErrorMessage());
} }
}); });
} }

View File

@@ -1,7 +1,14 @@
@model List<UserCollaborator> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<UserCollaborator>
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
<span class="lead">Collaborators</span> <span class="lead">@translator.Translate(userLanguage, "Collaborators")</span>
</div> </div>
<div class="col-4"> <div class="col-4">
<button onclick="addCollaborator()" class="btn btn-link btn-sm"><i class="bi bi-person-add"></i></button> <button onclick="addCollaborator()" class="btn btn-link btn-sm"><i class="bi bi-person-add"></i></button>
@@ -11,8 +18,8 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-8">Username</th> <th scope="col" class="col-8">@translator.Translate(userLanguage, "Username")</th>
<th scope="col" class="col-4">Delete</th> <th scope="col" class="col-4">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -37,7 +44,7 @@
if (data) { if (data) {
refreshCollaborators(); refreshCollaborators();
} else { } else {
errorToast("An error occurred, please try again later"); errorToast(genericErrorMessage());
} }
}) })
} }

View File

@@ -1,9 +1,14 @@
@model CollisionRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model CollisionRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Repair Record" : "Edit Repair Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Repair Record") : translator.Translate(userLanguage,"Edit Repair Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddCollisionRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddCollisionRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,31 +17,39 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="collisionRecordDate">Date</label> <label for="collisionRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="collisionRecordDate" class="form-control" placeholder="Date repair was performed" value="@Model.Date"> <input type="text" id="collisionRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date repair was performed")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="collisionRecordMileage">Odometer</label> <label for="collisionRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="collisionRecordMileage" class="form-control" placeholder="Odometer reading when repaired" value="@(isNew ? "" : Model.Mileage)"> <input type="number" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
<label for="collisionRecordDescription">Description</label> <label for="collisionRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="Description of item(s) repaired(i.e. Alternator)" value="@Model.Description"> <input type="text" id="collisionRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) repaired(i.e. Alternator)")" value="@Model.Description">
<label for="collisionRecordCost">Cost</label> <label for="collisionRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" id="collisionRecordCost" class="form-control" placeholder="Cost of the repair" value="@(isNew ? "" : Model.Cost)"> <input type="text" id="collisionRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the repair")" value="@(isNew ? "" : Model.Cost)">
@if (isNew) @if (isNew)
{ {
@await Html.PartialAsync("_SupplyStore", "RepairRecord") @await Html.PartialAsync("_SupplyStore", "RepairRecord")
} }
<label for="collisionRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="collisionRecordTag">
@foreach (string tag in Model.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="collisionRecordNotes">Notes(optional)</label> <label for="collisionRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="collisionRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
@@ -46,12 +59,13 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck"> <input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck"> <label class="form-check-label" for="addReminderCheck">
Add Reminder @translator.Translate(userLanguage,"Add Reminder")
</label> </label>
</div> </div>
} }
<label for="collisionRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -61,16 +75,26 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteCollisionRecord(@Model.Id)" style="margin-right:auto;">Delete</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">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">@translator.Translate(userLanguage,"Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'RepairRecord', 'ServiceRecord')">@translator.Translate(userLanguage,"Service Records")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'RepairRecord', 'UpgradeRecord')">@translator.Translate(userLanguage,"Upgrades")</a></li>
</ul>
</div>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddCollisionRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddCollisionRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle()">Add New Repair Record</button> <button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle()">@translator.Translate(userLanguage,"Add New Repair Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle(true)">Edit Repair Record</button> <button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Repair Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,33 +1,49 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var userLanguage = userConfig.UserLanguage;
} }
@model List<CollisionRecord> @model List<CollisionRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Repair Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Repair Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</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('accident-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>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <div class="btn-group">
<button onclick="showAddCollisionRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Repair Record</button> <button onclick="showAddCollisionRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Repair 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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">Import via CSV</a></li> <li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">Export to CSV</a></li> <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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddCollisionRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Repair Record</button> <button onclick="showAddCollisionRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Repair Record")</button>
} }
</div> </div>
</div> </div>
@@ -42,21 +58,21 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">Date</th> <th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">Odometer</th> <th scope="col" class="col-2">@translator.Translate(userLanguage,"Odometer")</th>
<th scope="col" class="col-3 col-xl-4">Description</th> <th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage,"Description")</th>
<th scope="col" class="col-2">Cost</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">Notes</th> <th scope="col" class="col-3">@translator.Translate(userLanguage,"Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (CollisionRecord collisionRecord in Model) @foreach (CollisionRecord collisionRecord in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)"> <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 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2">@collisionRecord.Mileage</td> <td class="col-2">@collisionRecord.Mileage</td>
<td class="col-3 col-xl-4">@collisionRecord.Description</td> <td class="col-3 col-xl-4">@collisionRecord.Description</td>
<td class="col-2">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</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> <td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
</tr> </tr>
} }

View File

@@ -1,4 +1,11 @@
@model CostMakeUpForVehicle @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model CostMakeUpForVehicle
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0) @if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
{ {
<canvas id="pie-chart"></canvas> <canvas id="pie-chart"></canvas>
@@ -9,17 +16,21 @@
new Chart($("#pie-chart"), { new Chart($("#pie-chart"), {
type: 'pie', type: 'pie',
data: { data: {
labels: ["Service Records", "Repairs", "Upgrades", "Tax", "Fuel"], labels: [decodeHTMLEntities('@translator.Translate(userLanguage, "Service Records")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Repairs")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Upgrades")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Tax")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Fuel")')],
datasets: [ datasets: [
{ {
label: "Expenses by Category", label: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Type")'),
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"], backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
data: [ data: [
@Model.ServiceRecordSum, globalParseFloat('@Model.ServiceRecordSum'),
@Model.CollisionRecordSum, globalParseFloat('@Model.CollisionRecordSum'),
@Model.UpgradeRecordSum, globalParseFloat('@Model.UpgradeRecordSum'),
@Model.TaxRecordSum, globalParseFloat('@Model.TaxRecordSum'),
@Model.GasRecordSum globalParseFloat('@Model.GasRecordSum')
] ]
} }
] ]
@@ -34,7 +45,7 @@
}, },
title: { title: {
display: true, display: true,
text: "Expenses by Type", text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Type")'),
color: useDarkMode ? "#fff" : "#000" color: useDarkMode ? "#fff" : "#000"
}, },
} }
@@ -46,6 +57,6 @@
else else
{ {
<div class="text-center"> <div class="text-center">
<h4>No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.</h4> <h4>@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")</h4>
</div> </div>
} }

View File

@@ -1,6 +1,7 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject IGasHelper gasHelper @inject IGasHelper gasHelper
@inject ITranslationHelper translator
@model GasRecordViewModelContainer @model GasRecordViewModelContainer
@{ @{
var userConfig = config.GetUserConfig(User); var userConfig = config.GetUserConfig(User);
@@ -10,58 +11,82 @@
var hideZero = userConfig.HideZero; var hideZero = userConfig.HideZero;
var useThreeDecimals = userConfig.UseThreeDecimalGasCost; var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
var gasCostFormat = useThreeDecimals ? "C3" : "C2"; var gasCostFormat = useThreeDecimals ? "C3" : "C2";
var userLanguage = userConfig.UserLanguage;
var useKwh = Model.UseKwh; var useKwh = Model.UseKwh;
var useHours = Model.UseHours;
var recordTags = Model.GasRecords.SelectMany(x => x.Tags).Distinct();
string preferredFuelEconomyUnit = userConfig.PreferredGasMileageUnit;
string preferredGasUnit = userConfig.PreferredGasUnit;
string consumptionUnit; string consumptionUnit;
string fuelEconomyUnit; string fuelEconomyUnit;
string distanceUnit = useMPG ? "mi." : "km"; string distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
if (useKwh) if (useKwh)
{ {
consumptionUnit = "kWh"; consumptionUnit = "kWh";
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km"; fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
} }
else if (useMPG && useUKMPG) else if (useMPG && useUKMPG)
{ {
consumptionUnit = "imp gal"; consumptionUnit = "imp gal";
fuelEconomyUnit = "mpg"; fuelEconomyUnit = useHours ? "h/g" : "mpg";
} else if (useUKMPG) } else if (useUKMPG)
{ {
fuelEconomyUnit = "l/100mi."; fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
consumptionUnit = "l"; consumptionUnit = "l";
distanceUnit = "mi."; distanceUnit = useHours ? "h" : "mi.";
} }
else else
{ {
consumptionUnit = useMPG ? "US gal" : "l"; consumptionUnit = useMPG ? "US gal" : "l";
fuelEconomyUnit = useMPG ? "mpg" : "l/100km"; fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
} }
} }
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# 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()) @if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
{ {
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {gasHelper.GetAverageGasMileage(Model.GasRecords)}")</span> <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">@($"Min Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span> if (useMPG)
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span> {
<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-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span> }
<span class="ms-2 badge bg-success">@($"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>
}
<datalist id="tagList">
@foreach (string recordTag in recordTags)
{
<!option value="@recordTag"></!option>
}
</datalist>
</div> </div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <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>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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('GasRecord')">Import via 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')">Export to 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>
</ul> </ul>
</div> </div>
} else { } else {
<button onclick="showAddGasRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>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>
} }
</div> </div>
</div> </div>
@@ -75,24 +100,26 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2">Date Refueled</th> <th scope="col" class="col-2">@translator.Translate(userLanguage,"Date Refueled")</th>
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th> <th scope="col" class="col-2">@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")</th>
<th scope="col" class="col-2">Consumption(@(consumptionUnit))</th> <th scope="col" class="col-1" style="cursor:pointer;" onclick="toggleSort('gas-tab-pane', this)">@($"Δ({distanceUnit})")</th>
<th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</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-1">Cost</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">Unit Cost</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>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (GasRecordViewModel gasRecord in Model.GasRecords) @foreach (GasRecordViewModel gasRecord in Model.GasRecords)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)"> <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">@gasRecord.Date</td>
<td class="col-2">@gasRecord.Mileage</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-2">@gasRecord.Gallons.ToString("F")</td> <td class="col-1">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td> <td class="col-2" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</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">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</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> </tr>
} }
</tbody> </tbody>
@@ -108,3 +135,15 @@
</div> </div>
</div> </div>
</div> </div>
<script>
@if (!string.IsNullOrWhiteSpace(preferredFuelEconomyUnit))
{
@:convertFuelMileageUnits(decodeHTMLEntities('@fuelEconomyUnit'), decodeHTMLEntities('@preferredFuelEconomyUnit'), false);
}
@if (!string.IsNullOrWhiteSpace(preferredGasUnit))
{
@:convertGasConsumptionUnits(decodeHTMLEntities('@consumptionUnit'), decodeHTMLEntities('@preferredGasUnit'), false);
}
</script>

View File

@@ -1,9 +1,14 @@
@model List<CostForVehicleByMonth> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<CostForVehicleByMonth>
@{ @{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" }; var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
var sortedByMPG = Model.OrderBy(x => x.Cost).ToList(); var sortedByMPG = Model.OrderBy(x => x.Cost).ToList();
} }
@if (Model.Any()) @if (Model.Where(x=>x.Cost > 0).Any())
{ {
<canvas id="bar-chart"></canvas> <canvas id="bar-chart"></canvas>
<script> <script>
@@ -16,8 +21,8 @@
var useDarkMode = getGlobalConfig().useDarkMode; var useDarkMode = getGlobalConfig().useDarkMode;
@foreach (CostForVehicleByMonth gasCost in Model) @foreach (CostForVehicleByMonth gasCost in Model)
{ {
@:barGraphLabels.push("@gasCost.MonthName"); @:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
@:barGraphData.push(@gasCost.Cost); @:barGraphData.push(globalParseFloat('@gasCost.Cost'));
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName); var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
@:barGraphColors.push('@barGraphColors[index]'); @:barGraphColors.push('@barGraphColors[index]');
} }
@@ -27,7 +32,7 @@
labels: barGraphLabels, labels: barGraphLabels,
datasets: [ datasets: [
{ {
label: "Expenses by Month", label: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")'),
backgroundColor: barGraphColors, backgroundColor: barGraphColors,
data: barGraphData data: barGraphData
} }
@@ -38,7 +43,7 @@
title: { title: {
display: true, display: true,
color: useDarkMode ? "#fff" : "#000", color: useDarkMode ? "#fff" : "#000",
text: 'Expenses by Month' text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")')
}, },
legend: { legend: {
display: false, display: false,
@@ -67,6 +72,6 @@
} else } else
{ {
<div class="text-center"> <div class="text-center">
<h4>No data found, insert/select some data to see visualizations here.</h4> <h4>@translator.Translate(userLanguage,"No data found, insert/select some data to see visualizations here.")</h4>
</div> </div>
} }

View File

@@ -1,10 +1,14 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@model GasRecordInputContainer @model GasRecordInputContainer
@{ @{
var useMPG = config.GetUserConfig(User).UseMPG; var userConfig = config.GetUserConfig(User);
var useUKMPG = config.GetUserConfig(User).UseUKMPG; var useMPG = userConfig.UseMPG;
var useUKMPG = userConfig.UseUKMPG;
var userLanguage = userConfig.UserLanguage;
var useKwh = Model.UseKwh; var useKwh = Model.UseKwh;
var useHours = Model.UseHours;
var isNew = Model.GasRecord.Id == 0; var isNew = Model.GasRecord.Id == 0;
string consumptionUnit; string consumptionUnit;
string distanceUnit; string distanceUnit;
@@ -19,7 +23,11 @@
{ {
consumptionUnit = useMPG ? "gallons" : "liters"; consumptionUnit = useMPG ? "gallons" : "liters";
} }
if (useUKMPG) if (useHours)
{
distanceUnit = "hours";
}
else if (useUKMPG)
{ {
distanceUnit = "miles"; distanceUnit = "miles";
} }
@@ -29,7 +37,7 @@
} }
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Gas Record") : translator.Translate(userLanguage,"Edit Gas Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -38,55 +46,64 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="gasRecordDate">Date</label> <label for="gasRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="gasRecordDate" placeholder="Date refueled" class="form-control" value="@Model.GasRecord.Date"> <input type="text" id="gasRecordDate" placeholder="@translator.Translate(userLanguage,"Date refueled")" class="form-control" value="@Model.GasRecord.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="gasRecordMileage">Odometer Reading(@distanceUnit)</label> <label for="gasRecordMileage">@($"{translator.Translate(userLanguage,"Odometer Reading")}({distanceUnit})")</label>
<input type="number" id="gasRecordMileage" class="form-control" placeholder="Odometer reading when refueled" value="@(isNew ? "" : Model.GasRecord.Mileage)"> <input type="number" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<label for="gasRecordGallons">Fuel Consumption(@(consumptionUnit))</label> <label for="gasRecordGallons">@($"{translator.Translate(userLanguage, "Fuel Consumption")}({consumptionUnit})")</label>
<input type="text" id="gasRecordGallons" class="form-control" placeholder="Amount of gas refueled" value="@(isNew ? "" : Model.GasRecord.Gallons)"> <input type="text" id="gasRecordGallons" class="form-control" placeholder="@translator.Translate(userLanguage,"Amount of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Gallons)">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="gasIsFillToFull" checked="@Model.GasRecord.IsFillToFull"> <input class="form-check-input" type="checkbox" role="switch" id="gasIsFillToFull" checked="@Model.GasRecord.IsFillToFull">
<label class="form-check-label" for="gasIsFillToFull">Is Filled To Full</label> <label class="form-check-label" for="gasIsFillToFull">@translator.Translate(userLanguage,"Is Filled To Full")</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="gasIsMissed" checked="@Model.GasRecord.MissedFuelUp"> <input class="form-check-input" type="checkbox" role="switch" id="gasIsMissed" checked="@Model.GasRecord.MissedFuelUp">
<label class="form-check-label" for="gasIsMissed">Missed Fuel Up(Skip MPG Calculation)</label> <label class="form-check-label" for="gasIsMissed">@translator.Translate(userLanguage,"Missed Fuel Up(Skip MPG Calculation)")</label>
</div> </div>
<label for="GasRecordCost">Cost</label> <label for="GasRecordCost">@translator.Translate(userLanguage,"Cost")</label>
@if (isNew) @if (isNew)
{ {
<div class="input-group"> <div class="input-group">
<input type="text" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)"> <input type="text" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of gas refueled")" value="@(isNew ? "" : Model.GasRecord.Cost)">
<div class="input-group-text"> <div class="input-group-text">
<select class="form-select form-select-sm" id="gasCostType"> <select class="form-select form-select-sm" id="gasCostType">
<option value="total">Total</option> <option value="total">@translator.Translate(userLanguage,"Total")</option>
<option value="unit">Unit</option> <option value="unit">@translator.Translate(userLanguage,"Unit")</option>
</select> </select>
</div> </div>
</div> </div>
} else } else
{ {
<input type="text" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)"> <input type="text" 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">
@foreach (string tag in Model.GasRecord.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="gasRecordNotes">Notes(optional)</label> <label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="gasRecordNotes" class="form-control" rows="5">@Model.GasRecord.Notes</textarea> <textarea id="gasRecordNotes" class="form-control" rows="5">@Model.GasRecord.Notes</textarea>
@if (Model.GasRecord.Files.Any()) @if (Model.GasRecord.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files) @await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
<label for="gasRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
{ {
<label for="gasRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -96,16 +113,16 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteGasRecord(@Model.GasRecord.Id)" style="margin-right:auto;">Delete</button> <button type="button" class="btn btn-danger" onclick="deleteGasRecord(@Model.GasRecord.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddGasRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddGasRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle()">Add New Gas Record</button> <button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle()">@translator.Translate(userLanguage,"Add New Gas Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle(true)">Edit Gas Record</button> <button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Gas Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,9 +1,14 @@
@model List<CostForVehicleByMonth> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<CostForVehicleByMonth>
@{ @{
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" }; var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
var sortedByMPG = Model.OrderByDescending(x => x.Cost).ToList(); var sortedByMPG = Model.OrderByDescending(x => x.Cost).ToList();
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
@if (Model.Any()) @if (Model.Where(x => x.Cost > 0).Any())
{ {
<canvas id="bar-chart-mpg"></canvas> <canvas id="bar-chart-mpg"></canvas>
@@ -17,8 +22,8 @@
var useDarkMode = getGlobalConfig().useDarkMode; var useDarkMode = getGlobalConfig().useDarkMode;
@foreach (CostForVehicleByMonth gasCost in Model) @foreach (CostForVehicleByMonth gasCost in Model)
{ {
@:barGraphLabels.push("@gasCost.MonthName"); @:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
@:barGraphData.push(@gasCost.Cost); @:barGraphData.push(globalParseFloat('@gasCost.Cost'));
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName); var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
@:barGraphColors.push('@barGraphColors[index]'); @:barGraphColors.push('@barGraphColors[index]');
} }
@@ -28,7 +33,7 @@
labels: barGraphLabels, labels: barGraphLabels,
datasets: [ datasets: [
{ {
label: "Fuel Mileage by Month", label: decodeHTMLEntities('@translator.Translate(userLanguage, "Fuel Mileage by Month")'),
backgroundColor: barGraphColors, backgroundColor: barGraphColors,
data: barGraphData data: barGraphData
} }
@@ -39,7 +44,7 @@
title: { title: {
display: true, display: true,
color: useDarkMode ? "#fff" : "#000", color: useDarkMode ? "#fff" : "#000",
text: 'Fuel Mileage by Month' text: decodeHTMLEntities('@translator.Translate(userLanguage, "Fuel Mileage by Month")')
}, },
legend: { legend: {
display: false, display: false,
@@ -65,9 +70,10 @@
}); });
} }
</script> </script>
} else }
else
{ {
<div class="text-center"> <div class="text-center">
<h4>No data found, insert/select some data to see visualizations here.</h4> <h4>@translator.Translate(userLanguage,"No data found, insert/select some data to see visualizations here.")</h4>
</div> </div>
} }

View File

@@ -1,9 +1,14 @@
@model Note @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model Note
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Note" : "Edit Note")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Note") : translator.Translate(userLanguage, "Edit Note"))</h5>
<button type="button" class="btn-close" onclick="hideAddNoteModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddNoteModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,13 +17,44 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="noteDescription">Description</label> <div class="form-check form-switch">
<input type="text" id="noteDescription" class="form-control" placeholder="Description of the note" value="@(isNew ? "" : Model.Description)"> <input class="form-check-input" type="checkbox" role="switch" id="noteIsPinned" checked="@Model.Pinned">
<label class="form-check-label" for="noteIsPinned">@translator.Translate(userLanguage,"Pinned")</label>
</div>
<label for="noteDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="noteDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of the note")" value="@(isNew ? "" : Model.Description)">
</div> </div>
<div class="col-12"> <div class="col-12">
<label for="noteTextArea">Notes</label> <label for="noteTextArea">@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 class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea> <textarea class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea>
</div> </div>
<div class="col-12">
@if (Model.Files.Any())
{
<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">
<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">
<br />
<small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
}
</div>
<div class="col-12">
<label for="noteRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="noteRecordTag">
@foreach (string tag in Model.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -26,19 +62,27 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteNote(@Model.Id)" style="margin-right:auto;">Delete</button> <button type="button" class="btn btn-danger" onclick="deleteNote(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddNoteModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddNoteModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveNoteToVehicle()">Add New Note</button> <button type="button" class="btn btn-primary" onclick="saveNoteToVehicle()">@translator.Translate(userLanguage,"Add New Note")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveNoteToVehicle(true)">Edit Note</button> <button type="button" class="btn btn-primary" onclick="saveNoteToVehicle(true)">@translator.Translate(userLanguage,"Edit Note")</button>
} }
</div> </div>
<script> <script>
var uploadedFiles = [];
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
function getNoteModelData(){ function getNoteModelData(){
return { id: @Model.Id} return { id: @Model.Id}
} }

View File

@@ -1,11 +1,29 @@
@model List<Note> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<Note>
@{
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Notes: {Model.Count()}")</span> <span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Notes")}: {Model.Count()}")</span>
@foreach (string recordTag in recordTags)
{
<span onclick="filterTable('notes-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>
<div> <div>
<button onclick="showAddNoteModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Note</button> <button onclick="showAddNoteModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Note")</button>
</div> </div>
</div> </div>
</div> </div>
@@ -19,16 +37,22 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-3">Description</th> <th scope="col" class="col-3">@translator.Translate(userLanguage,"Description")</th>
<th scope="col" class="col-9">Note</th> <th scope="col" class="col-9">@translator.Translate(userLanguage,"Note")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (Note note in Model) @foreach (Note note in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)"> <tr class="d-flex" style="cursor:pointer;" onclick="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>
} else
{
<td class="col-3">@note.Description</td> <td class="col-3">@note.Description</td>
<td class="col-9 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(note.NoteText, 100)</td> }
<td class="col-9 text-truncate" data-record-type="cost">@CarCareTracker.Helper.StaticHelper.TruncateStrings(note.NoteText, 100)</td>
</tr> </tr>
} }
</tbody> </tbody>

View File

@@ -1,9 +1,14 @@
@model OdometerRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model OdometerRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Odometer Record" : "Edit Odometer Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Odometer Record") : translator.Translate(userLanguage,"Edit Odometer Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddOdometerRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddOdometerRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,29 +17,38 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="odometerRecordDate">Date</label> <label for="odometerRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="odometerRecordDate" class="form-control" placeholder="Date recorded" value="@Model.Date"> <input type="text" id="odometerRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date recorded")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="odometerRecordMileage">Odometer</label> <label for="odometerRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="odometerRecordMileage" class="form-control" placeholder="Odometer reading" value="@(isNew ? "" : Model.Mileage)"> <input type="number" 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)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="odometerRecordNotes">Notes(optional)</label> <label for="odometerRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="odometerRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="odometerRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="odometerRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="odometerRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
{ {
<label for="odometerRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="odometerRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -44,16 +58,16 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteOdometerRecord(@Model.Id)" style="margin-right:auto;">Delete</button> <button type="button" class="btn btn-danger" onclick="deleteOdometerRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddOdometerRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddOdometerRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveOdometerRecordToVehicle()">Add New Odometer Record</button> <button type="button" class="btn btn-primary" onclick="saveOdometerRecordToVehicle()">@translator.Translate(userLanguage,"Add New Odometer Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveOdometerRecordToVehicle(true)">Edit Odometer Record</button> <button type="button" class="btn btn-primary" onclick="saveOdometerRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Odometer Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,32 +1,48 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
var userLanguage = userConfig.UserLanguage;
} }
@model List<OdometerRecord> @model List<OdometerRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# 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>
}
<datalist id="tagList">
@foreach (string recordTag in recordTags)
{
<!option value="@recordTag"></!option>
}
</datalist>
</div> </div>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <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>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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('OdometerRecord')">Import via 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')">Export to 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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>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>
</div> </div>
@@ -41,17 +57,17 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">Date</th> <th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-3">Odometer</th> <th scope="col" class="col-3">@translator.Translate(userLanguage,"Odometer")</th>
<th scope="col" class="col-7 col-xl-8">Notes</th> <th scope="col" class="col-7 col-xl-8">@translator.Translate(userLanguage,"Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (OdometerRecord odometerRecord in Model) @foreach (OdometerRecord odometerRecord in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditOdometerRecordModal(@odometerRecord.Id)"> <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-2 col-xl-1">@odometerRecord.Date.ToShortDateString()</td>
<td class="col-3">@odometerRecord.Mileage</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> <td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
</tr> </tr>
} }

View File

@@ -1,9 +1,14 @@
@model PlanRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model PlanRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Plan Record" : "Edit Plan Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Plan Record") : translator.Translate(userLanguage, "Edit Plan Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddPlanRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddPlanRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,53 +17,57 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="planRecordDescription">Description</label> <label for="planRecordDescription">@translator.Translate(userLanguage, "Description")</label>
<input type="text" id="planRecordDescription" class="form-control" placeholder="Describe the Plan" value="@Model.Description"> <input type="text" id="planRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage, "Describe the Plan")" value="@Model.Description">
<label for="planRecordCost">Cost</label> <label for="planRecordCost">@translator.Translate(userLanguage, "Cost")</label>
<input type="text" id="planRecordCost" class="form-control" placeholder="Cost of the Plan" value="@Model.Cost"> <input type="text" id="planRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage, "Cost of the Plan")" value="@Model.Cost">
@if (isNew) @if (isNew)
{ {
@await Html.PartialAsync("_SupplyStore", "PlanRecord") @await Html.PartialAsync("_SupplyStore", "PlanRecord")
} }
<label for="planRecordType">Type</label> <label for="planRecordType">@translator.Translate(userLanguage, "Type")</label>
<select class="form-select" id="planRecordType"> <select class="form-select" id="planRecordType">
<!option value="ServiceRecord" @(Model.ImportMode == ImportMode.ServiceRecord || isNew ? "selected" : "")>Service</!option> <!option value="ServiceRecord" @(Model.ImportMode == ImportMode.ServiceRecord || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Service")</!option>
<!option value="RepairRecord" @(Model.ImportMode == ImportMode.RepairRecord ? "selected" : "")>Repair</!option> <!option value="RepairRecord" @(Model.ImportMode == ImportMode.RepairRecord ? "selected" : "")>@translator.Translate(userLanguage, "Repair")</!option>
<!option value="UpgradeRecord" @(Model.ImportMode == ImportMode.UpgradeRecord ? "selected" : "")>Upgrade</!option> <!option value="UpgradeRecord" @(Model.ImportMode == ImportMode.UpgradeRecord ? "selected" : "")>@translator.Translate(userLanguage, "Upgrade")</!option>
</select> </select>
<label for="planRecordPriority">Priority</label> <label for="planRecordPriority">@translator.Translate(userLanguage, "Priority")</label>
<select class="form-select" id="planRecordPriority"> <select class="form-select" id="planRecordPriority">
<!option value="Critical" @(Model.Priority == PlanPriority.Critical ? "selected" : "")>Critical</!option> <!option value="Critical" @(Model.Priority == PlanPriority.Critical ? "selected" : "")>@translator.Translate(userLanguage, "Critical")</!option>
<!option value="Normal" @(Model.Priority == PlanPriority.Normal || isNew ? "selected" : "")>Normal</!option> <!option value="Normal" @(Model.Priority == PlanPriority.Normal || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Normal")</!option>
<!option value="Low" @(Model.Priority == PlanPriority.Low ? "selected" : "")>Low</!option> <!option value="Low" @(Model.Priority == PlanPriority.Low ? "selected" : "")>@translator.Translate(userLanguage, "Low")</!option>
</select> </select>
<label for="planRecordProgress">Current Stage</label> <label for="planRecordProgress">@translator.Translate(userLanguage, "Current Stage")</label>
<select class="form-select" id="planRecordProgress"> <select class="form-select" id="planRecordProgress">
<!option value="Backlog" @(Model.Progress == PlanProgress.Backlog ||isNew ? "selected" : "")>Planned</!option> <!option value = "Backlog" @(Model.Progress == PlanProgress.Backlog || isNew ? "selected" : "")>@translator.Translate(userLanguage, "Planned")</!option>
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>Doing</!option> <!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
<!option value="Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>Testing</!option> <!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
</select> </select>
@if (!isNew) @if (!isNew)
{ {
<label>Date Created: @Model.DateCreated</label> <label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>
<label>Last Modified: @Model.DateModified</label> <label>@($"{translator.Translate(userLanguage, "Last Modified")}: {Model.DateModified}")</label>
} }
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="planRecordNotes">Notes(optional)</label> <label for="planRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="planRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="planRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="planRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
{ {
<label for="planRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<br />
<small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -68,16 +77,25 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deletePlanRecord(@Model.Id)" style="margin-right:auto;">Delete</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()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddPlanRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="savePlanRecordToVehicle()">Add New Plan Record</button> <div class="btn-group">
<button onclick="savePlanRecordToVehicle()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add New Plan 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="savePlanRecordTemplate()">@translator.Translate(userLanguage, "Save as Template")</a></li>
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
</ul>
</div>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="savePlanRecordToVehicle(true)">Edit Plan Record</button> <button type="button" class="btn btn-primary" onclick="savePlanRecordToVehicle(true)">@translator.Translate(userLanguage, "Edit Plan Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -0,0 +1,87 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<PlanRecordInput>
<div class="modal-header">
<h5 class="modal-title">@translator.Translate(userLanguage,"Select Template")</h5>
<button type="button" class="btn-close" onclick="hidePlanRecordTemplatesModal()" aria-label="Close"></button>
</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-8">@translator.Translate(userLanguage,"Description")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Use")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody>
@foreach (PlanRecordInput planRecordTemplate in Model)
{
<tr class="d-flex" id="supplyRows">
<td class="col-8 text-truncate">
@StaticHelper.TruncateStrings(planRecordTemplate.Description)
@if (planRecordTemplate.Files.Any())
{
<i class="bi bi-paperclip ms-2"></i>
}
@if (planRecordTemplate.Supplies.Any())
{
<i class="bi bi-shop ms-2"></i>
}
@if (planRecordTemplate.ImportMode == ImportMode.ServiceRecord)
{
<i class="bi bi-card-checklist ms-2"></i>
}
else if (planRecordTemplate.ImportMode == ImportMode.UpgradeRecord)
{
<i class="bi bi-wrench-adjustable ms-2"></i>
}
else if (planRecordTemplate.ImportMode == ImportMode.RepairRecord)
{
<i class="bi bi-exclamation-octagon ms-2"></i>
}
@if (planRecordTemplate.Priority == PlanPriority.Critical)
{
<i class="bi bi-fire ms-2"></i>
}
else if (planRecordTemplate.Priority == PlanPriority.Normal)
{
<i class="bi bi-water ms-2"></i>
}
else if (planRecordTemplate.Priority == PlanPriority.Low)
{
<i class="bi bi-snow ms-2"></i>
}
</td>
<td class="col-2"><button type="button" class="btn btn-primary" onclick="usePlannerRecordTemplate(@planRecordTemplate.Id)"><i class="bi bi-plus-square"></i></button></td>
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deletePlannerRecordTemplate(@planRecordTemplate.Id)"><i class="bi bi-trash"></i></button></td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
else
{
<div class="row">
<div class="col-12">
<div class="text-center">
<h4>@translator.Translate(userLanguage, "No templates are found.")</h4>
</div>
</div>
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hidePlanRecordTemplatesModal()">@translator.Translate(userLanguage, "Cancel")</button>
</div>

View File

@@ -1,8 +1,11 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var userLanguage = userConfig.UserLanguage;
var backLogItems = Model.Where(x => x.Progress == PlanProgress.Backlog).OrderBy(x=>x.Priority); var backLogItems = Model.Where(x => x.Progress == PlanProgress.Backlog).OrderBy(x=>x.Priority);
var inProgressItems = Model.Where(x => x.Progress == PlanProgress.InProgress).OrderBy(x => x.Priority); var inProgressItems = Model.Where(x => x.Progress == PlanProgress.InProgress).OrderBy(x => x.Priority);
var testingItems = Model.Where(x => x.Progress == PlanProgress.Testing).OrderBy(x => x.Priority); var testingItems = Model.Where(x => x.Progress == PlanProgress.Testing).OrderBy(x => x.Priority);
@@ -12,25 +15,25 @@
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Plan Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage,"# of Plan Records")}: {Model.Count()}")</span>
</div> </div>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <div class="btn-group">
<button onclick="showAddPlanRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Plan Record</button> <button onclick="showAddPlanRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Plan 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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('PlanRecord')">Import via CSV</a></li> <li><a class="dropdown-item" href="#" onclick="showBulkImportModal('PlanRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('PlanRecord')">Export to CSV</a></li> <li><a class="dropdown-item" href="#" onclick="exportVehicleData('PlanRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddPlanRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Plan Record</button> <button onclick="showAddPlanRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Plan Record")</button>
} }
</div> </div>
</div> </div>
@@ -46,7 +49,7 @@
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')"> <div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">Planned</span> <span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
</div> </div>
</div> </div>
@foreach (PlanRecord planRecord in backLogItems) @foreach (PlanRecord planRecord in backLogItems)
@@ -57,7 +60,7 @@
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')"> <div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">Doing</span> <span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
</div> </div>
</div> </div>
@foreach (PlanRecord planRecord in inProgressItems) @foreach (PlanRecord planRecord in inProgressItems)
@@ -68,7 +71,7 @@
<div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Testing')"> <div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Testing')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">Testing</span> <span class="display-7">@translator.Translate(userLanguage,"Testing")</span>
</div> </div>
</div> </div>
@foreach (PlanRecord planRecord in testingItems) @foreach (PlanRecord planRecord in testingItems)
@@ -79,7 +82,7 @@
<div class="col-3 d-flex flex-column swimlane end" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')"> <div class="col-3 d-flex flex-column swimlane end" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')">
<div class="row"> <div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;"> <div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">Done</span> <span class="display-7">@translator.Translate(userLanguage,"Done")</span>
</div> </div>
</div> </div>
@foreach (PlanRecord planRecord in doneItems) @foreach (PlanRecord planRecord in doneItems)
@@ -98,3 +101,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" data-bs-focus="false" id="planRecordTemplateModal" 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="planRecordTemplateModalContent">
</div>
</div>
</div>

View File

@@ -1,4 +1,11 @@
@model ReminderMakeUpForVehicle @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model ReminderMakeUpForVehicle
@if (Model.UrgentCount + Model.VeryUrgentCount + Model.NotUrgentCount + Model.PastDueCount > 0) @if (Model.UrgentCount + Model.VeryUrgentCount + Model.NotUrgentCount + Model.PastDueCount > 0)
{ {
<canvas id="donut-chart"></canvas> <canvas id="donut-chart"></canvas>
@@ -9,10 +16,15 @@
new Chart($("#donut-chart"), { new Chart($("#donut-chart"), {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: ["Not Urgent", "Urgent", "Very Urgent", "Past Due"], labels: [
decodeHTMLEntities('@translator.Translate(userLanguage, "Not Urgent")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Urgent")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Very Urgent")'),
decodeHTMLEntities('@translator.Translate(userLanguage, "Past Due")')
],
datasets: [ datasets: [
{ {
label: "Reminders by Category", label: decodeHTMLEntities('@translator.Translate(userLanguage, "Reminders by Category")'),
backgroundColor: ["#488f31", "#ffa600", "#de425b", "#cccccc"], backgroundColor: ["#488f31", "#ffa600", "#de425b", "#cccccc"],
data: [ data: [
@Model.NotUrgentCount, @Model.NotUrgentCount,
@@ -33,7 +45,7 @@
}, },
title: { title: {
display: true, display: true,
text: "Reminders by Urgency", text: decodeHTMLEntities('@translator.Translate(userLanguage, "Reminders by Urgency")'),
color: useDarkMode ? "#fff" : "#000" color: useDarkMode ? "#fff" : "#000"
}, },
} }
@@ -45,6 +57,6 @@
else else
{ {
<div class="text-center"> <div class="text-center">
<h4>No data found, create reminders to see visualizations here.</h4> <h4>@translator.Translate(userLanguage,"No data found, create reminders to see visualizations here.")</h4>
</div> </div>
} }

View File

@@ -1,9 +1,14 @@
@model ReminderRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model ReminderRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Reminder" : "Edit Reminder")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Reminder") : translator.Translate(userLanguage, "Edit Reminder"))</h5>
<button type="button" class="btn-close" onclick="hideAddReminderRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddReminderRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,41 +17,42 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12" id="reminderOptions"> <div class="col-md-6 col-12" id="reminderOptions">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="reminderDescription">Description</label> <label for="reminderDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="reminderDescription" class="form-control" placeholder="Reminder Description" value="@Model.Description"> <input type="text" id="reminderDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Reminder Description")" value="@Model.Description">
<label>Remind me on:</label> <label>@translator.Translate(userLanguage,"Remind me on")</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricDate" value="@(ReminderMetric.Date)" checked="@(Model.Metric == ReminderMetric.Date)"> <input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricDate" value="@(ReminderMetric.Date)" checked="@(Model.Metric == ReminderMetric.Date)">
<label class="form-check-label" for="reminderMetricDate">Date</label> <label class="form-check-label" for="reminderMetricDate">@translator.Translate(userLanguage,"Date")</label>
</div> </div>
<div class="input-group"> <div class="input-group">
<input type="text" id="reminderDate" class="form-control" placeholder="Future Date" value="@Model.Date"> <input type="text" id="reminderDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Future Date")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricOdometer" value="@(ReminderMetric.Odometer)" checked="@(Model.Metric == ReminderMetric.Odometer)"> <input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricOdometer" value="@(ReminderMetric.Odometer)" checked="@(Model.Metric == ReminderMetric.Odometer)">
<label class="form-check-label" for="reminderMetricOdometer">Odometer</label> <label class="form-check-label" for="reminderMetricOdometer">@translator.Translate(userLanguage,"Odometer")</label>
</div> </div>
<div class="input-group"> <div class="input-group">
<input type="number" id="reminderMileage" class="form-control" placeholder="Future Odometer Reading" value="@(isNew ? "" : Model.Mileage)"> <input type="number" id="reminderMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Future Odometer Reading")" value="@(isNew ? "" : Model.Mileage)">
<div class="input-group-text"> <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" onclick="appendMileageToOdometer(500)">+500</button>
</div> </div>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricBoth" value="@(ReminderMetric.Both)" checked="@(Model.Metric == ReminderMetric.Both)"> <input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricBoth" value="@(ReminderMetric.Both)" checked="@(Model.Metric == ReminderMetric.Both)">
<label class="form-check-label" for="reminderMetricBoth">Whichever comes first</label> <label class="form-check-label" for="reminderMetricBoth">@translator.Translate(userLanguage,"Whichever comes first")</label>
</div> </div>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="reminderNotes">Notes(optional)</label> <label for="reminderNotes">@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>
<textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" onChange="enableRecurring()" role="switch" id="reminderIsRecurring" checked="@Model.IsRecurring"> <input class="form-check-input" type="checkbox" onChange="enableRecurring()" role="switch" id="reminderIsRecurring" checked="@Model.IsRecurring">
<label class="form-check-label" for="reminderIsRecurring">Is Recurring</label> <label class="form-check-label" for="reminderIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label>
</div> </div>
<label for="reminderRecurringMileage">Odometer</label> <label for="reminderRecurringMileage">@translator.Translate(userLanguage,"Odometer")</label>
<select class="form-select" id="reminderRecurringMileage" @(Model.IsRecurring ? "" : "disabled")> <select class="form-select" onchange="checkCustomMileageInterval()" id="reminderRecurringMileage" @(Model.IsRecurring ? "" : "disabled")>
<!option value="Other" @(Model.ReminderMileageInterval == ReminderMileageInterval.Other ? "selected" : "")>@(Model.ReminderMileageInterval == ReminderMileageInterval.Other && Model.CustomMileageInterval > 0 ? $"{translator.Translate(userLanguage, "Other")}: {Model.CustomMileageInterval}" : $"{translator.Translate(userLanguage, "Other")}") </!option>
<!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option> <!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option>
<!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option> <!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option>
<!option value="FiveHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiveHundredMiles ? "selected" : "")>500 mi. / Km</!option> <!option value="FiveHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiveHundredMiles ? "selected" : "")>500 mi. / Km</!option>
@@ -59,18 +65,20 @@
<!option value="FifteenThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FifteenThousandMiles ? "selected" : "")>15000 mi. / Km</!option> <!option value="FifteenThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FifteenThousandMiles ? "selected" : "")>15000 mi. / Km</!option>
<!option value="TwentyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.TwentyThousandMiles ? "selected" : "")>20000 mi. / Km</!option> <!option value="TwentyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.TwentyThousandMiles ? "selected" : "")>20000 mi. / Km</!option>
<!option value="ThirtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.ThirtyThousandMiles ? "selected" : "")>30000 mi. / Km</!option> <!option value="ThirtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.ThirtyThousandMiles ? "selected" : "")>30000 mi. / Km</!option>
<!option value="FortyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FortyThousandMiles ? "selected" : "")>40000 mi. / Km</!option>
<!option value="FiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyThousandMiles ? "selected" : "")>50000 mi. / Km</!option> <!option value="FiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyThousandMiles ? "selected" : "")>50000 mi. / Km</!option>
<!option value="SixtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.SixtyThousandMiles ? "selected" : "")>60000 mi. / Km</!option>
<!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option> <!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option>
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option> <!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
</select> </select>
<label for="reminderRecurringMonth">Month</label> <label for="reminderRecurringMonth">Month</label>
<select class="form-select" id="reminderRecurringMonth" @(Model.IsRecurring ? "" : "disabled")> <select class="form-select" id="reminderRecurringMonth" @(Model.IsRecurring ? "" : "disabled")>
<!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>3 Months</!option> <!option value="ThreeMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage,"3 Months")</!option>
<!option value="SixMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>6 Months</!option> <!option value="SixMonths" @(Model.ReminderMonthInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage,"6 Months")</!option>
<!option value="OneYear" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneYear ? "selected" : "")>1 Year</!option> <!option value="OneYear" @(Model.ReminderMonthInterval == ReminderMonthInterval.OneYear ? "selected" : "")>@translator.Translate(userLanguage, "1 Year")</!option>
<!option value="TwoYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.TwoYears ? "selected" : "")>2 Years</!option> <!option value="TwoYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.TwoYears ? "selected" : "")>@translator.Translate(userLanguage, "2 Years")</!option>
<!option value="ThreeYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeYears ? "selected" : "")>3 Years</!option> <!option value="ThreeYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeYears ? "selected" : "")>@translator.Translate(userLanguage, "3 Years")</!option>
<!option value="FiveYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.FiveYears ? "selected" : "")>5 Years</!option> <!option value="FiveYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.FiveYears ? "selected" : "")>@translator.Translate(userLanguage, "5 Years")</!option>
</select> </select>
</div> </div>
</div> </div>
@@ -80,20 +88,21 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@Model.Id)" style="margin-right:auto;">Delete</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()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddReminderRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle()">Add New Reminder</button> <button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle()">@translator.Translate(userLanguage, "Add New Reminder")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle(true)">Edit Reminder</button> <button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle(true)">@translator.Translate(userLanguage, "Edit Reminder")</button>
} }
</div> </div>
<script> <script>
var customMileageInterval = @Model.CustomMileageInterval;
function getReminderRecordModelData() { function getReminderRecordModelData() {
return { id: @Model.Id} return { id: @Model.Id, mileageInterval: '@Model.ReminderMileageInterval.ToString()'}
} }
</script> </script>

View File

@@ -1,18 +1,23 @@
@model List<ReminderRecordViewModel> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<ReminderRecordViewModel>
@{ @{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var hasRefresh = Model.Where(x => (x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue) && x.IsRecurring).Any(); var hasRefresh = Model.Where(x => (x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue) && x.IsRecurring).Any();
} }
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Reminders: {Model.Count()}")</span> <span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage, "# of Reminders")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-secondary">@($"Past Due: {Model.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()}")</span> <span class="ms-2 badge bg-secondary">@($"{translator.Translate(userLanguage, "Past Due")}: {Model.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()}")</span>
<span class="ms-2 badge bg-danger">@($"Very Urgent: {Model.Where(x=>x.Urgency == ReminderUrgency.VeryUrgent).Count()}")</span> <span class="ms-2 badge bg-danger">@($"{translator.Translate(userLanguage, "Very Urgent")}: {Model.Where(x=>x.Urgency == ReminderUrgency.VeryUrgent).Count()}")</span>
<span class="ms-2 badge bg-warning">@($"Urgent: {Model.Where(x => x.Urgency == ReminderUrgency.Urgent).Count()}")</span> <span class="ms-2 badge bg-warning">@($"{translator.Translate(userLanguage, "Urgent")}: {Model.Where(x => x.Urgency == ReminderUrgency.Urgent).Count()}")</span>
<span class="ms-2 badge bg-success">@($"Not Urgent: {Model.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count()}")</span> <span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage, "Not Urgent")}: {Model.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count()}")</span>
</div> </div>
<div> <div>
<button onclick="showAddReminderModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Reminder</button> <button onclick="showAddReminderModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Reminder")</button>
</div> </div>
</div> </div>
</div> </div>
@@ -26,15 +31,15 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-1">Urgency</th> <th scope="col" class="col-1">@translator.Translate(userLanguage, "Urgency")</th>
<th scope="col" class="col-2">Metric</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Metric")</th>
<th scope="col" class="@(hasRefresh ? "col-4" : "col-5")">Description</th> <th scope="col" class="@(hasRefresh ? "col-4" : "col-5")">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-3">Notes</th> <th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
@if (hasRefresh) @if (hasRefresh)
{ {
<th scope="col" class="col-1">Done</th> <th scope="col" class="col-1">@translator.Translate(userLanguage, "Done")</th>
} }
<th scope="col" class="col-1">Delete</th> <th scope="col" class="col-1">@translator.Translate(userLanguage, "Delete")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -43,19 +48,19 @@
<tr class="d-flex" style="cursor:pointer;" onclick="showEditReminderRecordModal(@reminderRecord.Id)"> <tr class="d-flex" style="cursor:pointer;" onclick="showEditReminderRecordModal(@reminderRecord.Id)">
@if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent) @if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent)
{ {
<td class="col-1"><span class="badge text-bg-danger">Very Urgent</span></td> <td class="col-1"><span class="badge text-bg-danger">@translator.Translate(userLanguage, "Very Urgent")</span></td>
} }
else if (reminderRecord.Urgency == ReminderUrgency.Urgent) else if (reminderRecord.Urgency == ReminderUrgency.Urgent)
{ {
<td class="col-1"><span class="badge text-bg-warning">Urgent</span></td> <td class="col-1"><span class="badge text-bg-warning">@translator.Translate(userLanguage, "Urgent")</span></td>
} }
else if (reminderRecord.Urgency == ReminderUrgency.PastDue) else if (reminderRecord.Urgency == ReminderUrgency.PastDue)
{ {
<td class="col-1"><span class="badge text-bg-secondary">Past Due</span></td> <td class="col-1"><span class="badge text-bg-secondary">@translator.Translate(userLanguage, "Past Due")</span></td>
} }
else else
{ {
<td class="col-1"><span class="badge text-bg-success">Not Urgent</span></td> <td class="col-1"><span class="badge text-bg-success">@translator.Translate(userLanguage, "Not Urgent")</span></td>
} }
@if (reminderRecord.Metric == ReminderMetric.Date) @if (reminderRecord.Metric == ReminderMetric.Date)
{ {

View File

@@ -1,4 +1,11 @@
@model ReportViewModel @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model ReportViewModel
<div class="container reportTabContainer"> <div class="container reportTabContainer">
<div class="row hideOnPrint"> <div class="row hideOnPrint">
<div class="col-md-3 col-12 mt-2"> <div class="col-md-3 col-12 mt-2">
@@ -25,23 +32,23 @@
<div class="col-12 col-md-10 reportsCheckBoxContainer"> <div class="col-12 col-md-10 reportsCheckBoxContainer">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked> <input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
<label class="form-check-label" for="serviceExpenseCheck">Service</label> <label class="form-check-label" for="serviceExpenseCheck">@translator.Translate(userLanguage,"Service")</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked> <input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
<label class="form-check-label" for="repairExpenseCheck">Repairs</label> <label class="form-check-label" for="repairExpenseCheck">@translator.Translate(userLanguage, "Repairs")</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked> <input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
<label class="form-check-label" for="upgradeExpenseCheck">Upgrades</label> <label class="form-check-label" for="upgradeExpenseCheck">@translator.Translate(userLanguage, "Upgrades")</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked> <input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
<label class="form-check-label" for="gasExpenseCheck">Gas</label> <label class="form-check-label" for="gasExpenseCheck">@translator.Translate(userLanguage, "Fuel")</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked> <input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
<label class="form-check-label" for="taxExpenseCheck">Tax</label> <label class="form-check-label" for="taxExpenseCheck">@translator.Translate(userLanguage, "Taxes")</label>
</div> </div>
</div> </div>
<div class="col-md-1 d-sm-none d-md-block"></div> <div class="col-md-1 d-sm-none d-md-block"></div>
@@ -56,10 +63,10 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<select class="form-select" onchange="updateReminderPie()" id="reminderOption"> <select class="form-select" onchange="updateReminderPie()" id="reminderOption">
<option value="0">As of Today</option> <option value="0">@translator.Translate(userLanguage, "As of Today")</option>
<option value="30">+30 Days</option> <option value="30">@translator.Translate(userLanguage, "+30 Days")</option>
<option value="60">+60 Days</option> <option value="60">@translator.Translate(userLanguage, "+60 Days")</option>
<option value="90">+90 Days</option> <option value="90">@translator.Translate(userLanguage, "+90 Days")</option>
</select> </select>
</div> </div>
</div> </div>
@@ -81,8 +88,11 @@
</div> </div>
</div> </div>
<div class="col-md-3 col-12 chartContainer"> <div class="col-md-3 col-12 chartContainer">
<div class="d-flex justify-content-center"> <div class="d-grid">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">Vehicle Maintenance Report<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button> <button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Vehicle Maintenance Report")<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
<div class="d-grid">
<button onclick="exportAttachments()" class="btn btn-secondary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Export Attachments")<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,14 @@
@model ServiceRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model ServiceRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Service Record" : "Edit Service Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Service Record") : translator.Translate(userLanguage,"Edit Service Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddServiceRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddServiceRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,31 +17,39 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="serviceRecordDate">Date</label> <label for="serviceRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="serviceRecordDate" class="form-control" placeholder="Date service was performed" value="@Model.Date"> <input type="text" id="serviceRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date service was performed")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="serviceRecordMileage">Odometer</label> <label for="serviceRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="serviceRecordMileage" class="form-control" placeholder="Odometer reading when serviced" value="@(isNew ? "" : Model.Mileage)"> <input type="number" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew ? "" : Model.Mileage)">
<label for="serviceRecordDescription">Description</label> <label for="serviceRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="serviceRecordDescription" class="form-control" placeholder="Description of item(s) serviced(i.e. Oil Change)" value="@Model.Description"> <input type="text" id="serviceRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) serviced(i.e. Oil Change)")" value="@Model.Description">
<label for="serviceRecordCost">Cost</label> <label for="serviceRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" id="serviceRecordCost" class="form-control" placeholder="Cost of the service" value="@(isNew ? "" : Model.Cost)"> <input type="text" id="serviceRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the service")" value="@(isNew ? "" : Model.Cost)">
@if (isNew) @if (isNew)
{ {
@await Html.PartialAsync("_SupplyStore", "ServiceRecord") @await Html.PartialAsync("_SupplyStore", "ServiceRecord")
} }
<label for="serviceRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="serviceRecordTag">
@foreach(string tag in Model.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="serviceRecordNotes">Notes(optional)</label> <label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="serviceRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
@@ -46,12 +59,13 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck"> <input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck"> <label class="form-check-label" for="addReminderCheck">
Add Reminder @translator.Translate(userLanguage,"Add Reminder")
</label> </label>
</div> </div>
} }
<label for="serviceRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -61,16 +75,26 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteServiceRecord(@Model.Id)" style="margin-right:auto;">Delete</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">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">@translator.Translate(userLanguage,"Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'ServiceRecord', 'RepairRecord')">@translator.Translate(userLanguage,"Repairs")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'ServiceRecord', 'UpgradeRecord')">@translator.Translate(userLanguage,"Upgrades")</a></li>
</ul>
</div>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddServiceRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddServiceRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle()">Add New Service Record</button> <button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle()">@translator.Translate(userLanguage,"Add New Service Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle(true)">Edit Service Record</button> <button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Service Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,33 +1,49 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var userLanguage = userConfig.UserLanguage;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
} }
@model List<ServiceRecord> @model List<ServiceRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Service Records: {Model.Count()}")</span> <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">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</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>
}
<datalist id="tagList">
@foreach (string recordTag in recordTags)
{
<!option value="@recordTag"></!option>
}
</datalist>
</div> </div>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <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>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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('ServiceRecord')">Import via 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')">Export to 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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddServiceRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>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>
</div> </div>
@@ -42,21 +58,21 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">Date</th> <th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">Odometer</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4">Description</th> <th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">Cost</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">Notes</th> <th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (ServiceRecord serviceRecord in Model) @foreach (ServiceRecord serviceRecord in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditServiceRecordModal(@serviceRecord.Id)"> <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 col-xl-1">@serviceRecord.Date.ToShortDateString()</td>
<td class="col-2">@serviceRecord.Mileage</td> <td class="col-2">@serviceRecord.Mileage</td>
<td class="col-3 col-xl-4">@serviceRecord.Description</td> <td class="col-3 col-xl-4">@serviceRecord.Description</td>
<td class="col-2">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</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> <td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(serviceRecord.Notes)</td>
</tr> </tr>
} }

View File

@@ -1,9 +1,14 @@
@model SupplyRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model SupplyRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Supply Record" : "Edit Supply Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Supply Record") : translator.Translate(userLanguage,"Edit Supply Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddSupplyRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddSupplyRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,43 +17,45 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="supplyRecordDate">Date</label> <label for="supplyRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="supplyRecordDate" class="form-control" placeholder="Date purchased" value="@Model.Date"> <input type="text" id="supplyRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date purchased")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="supplyRecordPartNumber">Part Number</label> <label for="supplyRecordPartNumber">@translator.Translate(userLanguage,"Part Number")</label>
<input type="text" id="supplyRecordPartNumber" class="form-control" placeholder="Part #/Model #/SKU #" value="@(isNew ? "" : Model.PartNumber)"> <input type="text" id="supplyRecordPartNumber" class="form-control" placeholder="@translator.Translate(userLanguage,"Part #/Model #/SKU #")" value="@(isNew ? "" : Model.PartNumber)">
<label for="supplyRecordDescription">Description</label> <label for="supplyRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="supplyRecordDescription" class="form-control" placeholder="Description of the Part/Supplies" value="@Model.Description"> <input type="text" id="supplyRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of the Part/Supplies")" value="@Model.Description">
<label for="supplyRecordSupplier">Supplier/Vendor</label> <label for="supplyRecordSupplier">@translator.Translate(userLanguage,"Supplier/Vendor")</label>
<input type="text" id="supplyRecordSupplier" class="form-control" placeholder="Part Supplier" value="@Model.PartSupplier"> <input type="text" id="supplyRecordSupplier" class="form-control" placeholder="@translator.Translate(userLanguage,"Part Supplier")" value="@Model.PartSupplier">
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="supplyRecordQuantity">Quantity</label> <label for="supplyRecordQuantity">@translator.Translate(userLanguage,"Quantity")</label>
<input type="text" id="supplyRecordQuantity" class="form-control" placeholder="Quantity" value="@(isNew ? "1" : Model.Quantity)"> <input type="text" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="supplyRecordCost">Cost</label> <label for="supplyRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" id="supplyRecordCost" class="form-control" placeholder="Cost" value="@(isNew ? "" : Model.Cost)"> <input type="text" id="supplyRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost")" value="@(isNew ? "" : Model.Cost)">
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="supplyRecordNotes">Notes(optional)</label> <label for="supplyRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="supplyRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="supplyRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="supplyRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
{ {
<label for="supplyRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -58,16 +65,16 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteSupplyRecord(@Model.Id)" style="margin-right:auto;">Delete</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()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddSupplyRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle()">Add New Supply Record</button> <button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle()">@translator.Translate(userLanguage,"Add New Supply Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle(true)">Edit Supply Record</button> <button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Supply Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,33 +1,38 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var userLanguage = userConfig.UserLanguage;
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
} }
@model List<SupplyRecord> @model List<SupplyRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Supply Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success">@($"{translator.Translate(userLanguage,"# of Supply Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span> <span class="ms-2 badge bg-primary">@($"{translator.Translate(userLanguage,"Total")}: {Model.Sum(x => x.Cost).ToString("C")}")</span>
</div> </div>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <div class="btn-group">
<button onclick="showAddSupplyRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Supply Record</button> <button onclick="showAddSupplyRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Supply 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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('SupplyRecord')">Import via CSV</a></li> <li><a class="dropdown-item" href="#" onclick="showBulkImportModal('SupplyRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('SupplyRecord')">Export to CSV</a></li> <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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddSupplyRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Supply Record</button> <button onclick="showAddSupplyRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Supply Record")</button>
} }
</div> </div>
</div> </div>
@@ -42,13 +47,13 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">Date</th> <th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
<th scope="col" class="col-2">Part #</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Part #")</th>
<th scope="col" class="col-2">Supplier</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Supplier")</th>
<th scope="col" class="col-2 col-xl-3">Description</th> <th scope="col" class="col-2 col-xl-3">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-1">Quantity</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">Cost</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">Notes</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,7 +1,14 @@
@model string @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model string
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<a onclick="showSuppliesModal()" class="btn btn-link">Choose Supplies</a> <a onclick="showSuppliesModal()" class="btn btn-link">@translator.Translate(userLanguage,"Choose Supplies")</a>
</div> </div>
</div> </div>
<script> <script>

View File

@@ -1,6 +1,13 @@
@model List<SupplyRecord> @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<SupplyRecord>
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Select Supplies</h5> <h5 class="modal-title">@translator.Translate(userLanguage,"Select Supplies")</h5>
<button type="button" class="btn-close" onclick="hideSuppliesModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideSuppliesModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -9,17 +16,16 @@
<div class="row"> <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;">
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
Supplies are requisitioned immediately after the record is created and cannot be modified. @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.")
If you have incorrectly entered the amount you needed you will need to correct it in the Supplies tab.
</div> </div>
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-1"></th> <th scope="col" class="col-1"></th>
<th scope="col" class="col-2">Quantity.</th> <th scope="col" class="col-2">@translator.Translate(userLanguage,"Quantity")</th>
<th scope="col" class="col-2">In Stock</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "In Stock")</th>
<th scope="col" class="col-5">Description</th> <th scope="col" class="col-5">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">Unit Cost</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Unit Cost")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -43,7 +49,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="text-center"> <div class="text-center">
<h4>No supplies with quantities greater than 0 is found.</h4> <h4>@translator.Translate(userLanguage, "No supplies with quantities greater than 0 is found.")</h4>
</div> </div>
</div> </div>
</div> </div>
@@ -51,8 +57,8 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span id="supplySumLabel" style="margin-right:auto;">Total: 0.00</span> <span id="supplySumLabel" style="margin-right:auto;">Total: 0.00</span>
<button type="button" class="btn btn-secondary" onclick="hideSuppliesModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideSuppliesModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" class="btn btn-primary" disabled id="selectSuppliesButton" onclick="selectSupplies()">Select</button> <button type="button" class="btn btn-primary" disabled id="selectSuppliesButton" onclick="selectSupplies()">@translator.Translate(userLanguage, "Select")</button>
</div> </div>
<script> <script>
function recalculateTotal() { function recalculateTotal() {

View File

@@ -1,9 +1,14 @@
@model TaxRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model TaxRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Tax Record" : "Edit Tax Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Tax Record") : translator.Translate(userLanguage,"Edit Tax Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddTaxRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddTaxRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,25 +17,47 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="taxRecordDate">Date</label> <label for="taxRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="taxRecordDate" class="form-control" placeholder="Date tax was paid" value="@Model.Date"> <input type="text" id="taxRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date tax was paid")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="taxRecordDescription">Description</label> <label for="taxRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="taxRecordDescription" class="form-control" placeholder="Description of tax paid(i.e. Registration)" value="@Model.Description"> <input type="text" id="taxRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of tax paid(i.e. Registration)")" value="@Model.Description">
<label for="taxRecordCost">Cost</label> <label for="taxRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" id="taxRecordCost" class="form-control" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)"> <input type="text" 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)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="taxRecordNotes">Notes(optional)</label> <label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring">
<label class="form-check-label" for="taxIsRecurring">@translator.Translate(userLanguage,"Is Recurring")</label>
</div>
<label for="taxRecurringMonth">@translator.Translate(userLanguage,"Month")</label>
<select class="form-select" id="taxRecurringMonth" @(Model.IsRecurring ? "" : "disabled")>
<!option value="OneMonth" @(Model.RecurringInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>@translator.Translate(userLanguage,"1 Month")</!option>
<!option value="ThreeMonths" @(Model.RecurringInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>@translator.Translate(userLanguage, "3 Months")</!option>
<!option value="SixMonths" @(Model.RecurringInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>@translator.Translate(userLanguage, "6 Months")</!option>
<!option value="OneYear" @(Model.RecurringInterval == ReminderMonthInterval.OneYear ? "selected" : "")>@translator.Translate(userLanguage, "1 Year")</!option>
<!option value="TwoYears" @(Model.RecurringInterval == ReminderMonthInterval.TwoYears ? "selected" : "")>@translator.Translate(userLanguage, "2 Years")</!option>
<!option value="ThreeYears" @(Model.RecurringInterval == ReminderMonthInterval.ThreeYears ? "selected" : "")>@translator.Translate(userLanguage, "3 Years")</!option>
<!option value="FiveYears" @(Model.RecurringInterval == ReminderMonthInterval.FiveYears ? "selected" : "")>@translator.Translate(userLanguage, "5 Years")</!option>
</select>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="taxRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
@@ -40,12 +67,13 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck"> <input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck"> <label class="form-check-label" for="addReminderCheck">
Add Reminder @translator.Translate(userLanguage,"Add Reminder")
</label> </label>
</div> </div>
} }
<label for="taxRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -55,16 +83,16 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteTaxRecord(@Model.Id)" style="margin-right:auto;">Delete</button> <button type="button" class="btn btn-danger" onclick="deleteTaxRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddTaxRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddTaxRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle()">Add New Tax Record</button> <button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle()">@translator.Translate(userLanguage,"Add New Tax Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle(true)">Edit Tax Record</button> <button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Tax Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,33 +1,49 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var userLanguage = userConfig.UserLanguage;
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
} }
@model List<TaxRecord> @model List<TaxRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Tax Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Tax Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</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('tax-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>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <div class="btn-group">
<button onclick="showAddTaxRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Tax Record</button> <button onclick="showAddTaxRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Tax 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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('TaxRecord')">Import via CSV</a></li> <li><a class="dropdown-item" href="#" onclick="showBulkImportModal('TaxRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('TaxRecord')">Export to CSV</a></li> <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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddTaxRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Tax Record</button> <button onclick="showAddTaxRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Tax Record")</button>
} }
</div> </div>
</div> </div>
@@ -42,19 +58,19 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-3 col-xl-1">Date</th> <th scope="col" class="col-3 col-xl-1">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-4 col-xl-6">Description</th> <th scope="col" class="col-4 col-xl-6">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">Cost</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">Notes</th> <th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (TaxRecord taxRecord in Model) @foreach (TaxRecord taxRecord in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)"> <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-3 col-xl-1">@taxRecord.Date.ToShortDateString()</td>
<td class="col-4 col-xl-6">@taxRecord.Description</td> <td class="col-4 col-xl-6">@taxRecord.Description</td>
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</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> <td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(taxRecord.Notes)</td>
</tr> </tr>
} }

View File

@@ -1,9 +1,14 @@
@model UpgradeRecordInput @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model UpgradeRecordInput
@{ @{
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Upgrade Record" : "Edit Upgrade Record")</h5> <h5 class="modal-title">@(isNew ? translator.Translate(userLanguage,"Add New Upgrade Record") : translator.Translate(userLanguage,"Edit Upgrade Record"))</h5>
<button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -12,31 +17,39 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="upgradeRecordDate">Date</label> <label for="upgradeRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="upgradeRecordDate" class="form-control" placeholder="Date upgrade/mods was installed" value="@Model.Date"> <input type="text" id="upgradeRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"Date upgrade/mods was installed")" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="upgradeRecordMileage">Odometer</label> <label for="upgradeRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
<input type="number" id="upgradeRecordMileage" class="form-control" placeholder="Odometer reading when upgraded/modded" value="@(isNew ? "" : Model.Mileage)"> <input type="number" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew ? "" : Model.Mileage)">
<label for="upgradeRecordDescription">Description</label> <label for="upgradeRecordDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="Description of item(s) upgraded/modded" value="@Model.Description"> <input type="text" id="upgradeRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) upgraded/modded")" value="@Model.Description">
<label for="upgradeRecordCost">Cost</label> <label for="upgradeRecordCost">@translator.Translate(userLanguage,"Cost")</label>
<input type="text" id="upgradeRecordCost" class="form-control" placeholder="Cost of the upgrade/mods" value="@(isNew ? "" : Model.Cost)"> <input type="text" id="upgradeRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the upgrade/mods")" value="@(isNew ? "" : Model.Cost)">
@if (isNew) @if (isNew)
{ {
@await Html.PartialAsync("_SupplyStore", "UpgradeRecord") @await Html.PartialAsync("_SupplyStore", "UpgradeRecord")
} }
<label for="upgradeRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
<select multiple class="form-select" id="upgradeRecordTag">
@foreach (string tag in Model.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<label for="upgradeRecordNotes">Notes(optional)</label> <label for="upgradeRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any()) @if (Model.Files.Any())
{ {
<div> <div>
@await Html.PartialAsync("_UploadedFiles", Model.Files) @await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="upgradeRecordFiles">Upload more documents</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
</div> </div>
} }
else else
@@ -46,12 +59,13 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck"> <input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck"> <label class="form-check-label" for="addReminderCheck">
Add Reminder @translator.Translate(userLanguage,"Add Reminder")
</label> </label>
</div> </div>
} }
<label for="upgradeRecordFiles">Upload documents(optional)</label> <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=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage,"Max File Size: 28.6MB")</small>
} }
</div> </div>
</div> </div>
@@ -61,16 +75,26 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @if (!isNew)
{ {
<button type="button" class="btn btn-danger" onclick="deleteUpgradeRecord(@Model.Id)" style="margin-right:auto;">Delete</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">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">@translator.Translate(userLanguage,"Move To")</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'UpgradeRecord', 'ServiceRecord')">@translator.Translate(userLanguage,"Service Records")</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'UpgradeRecord', 'RepairRecord')">@translator.Translate(userLanguage,"Repairs")</a></li>
</ul>
</div>
} }
<button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle()">Add New Upgrade Record</button> <button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle()">@translator.Translate(userLanguage,"Add New Upgrade Record")</button>
} }
else if (!isNew) else if (!isNew)
{ {
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">Edit Upgrade Record</button> <button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Upgrade Record")</button>
} }
</div> </div>
<script> <script>

View File

@@ -1,33 +1,49 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; var userConfig = config.GetUserConfig(User);
var hideZero = config.GetUserConfig(User).HideZero; var userLanguage = userConfig.UserLanguage;
var enableCsvImports = userConfig.EnableCsvImports;
var hideZero = userConfig.HideZero;
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
} }
@model List<UpgradeRecord> @model List<UpgradeRecord>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Upgrade Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Upgrade Records")}: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</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('upgrade-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>
<div> <div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
<div class="btn-group"> <div class="btn-group">
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button> <button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Upgrade 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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('UpgradeRecord')">Import via CSV</a></li> <li><a class="dropdown-item" href="#" onclick="showBulkImportModal('UpgradeRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('UpgradeRecord')">Export to CSV</a></li> <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>
</ul> </ul>
</div> </div>
} }
else else
{ {
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button> <button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Upgrade Record")</button>
} }
</div> </div>
</div> </div>
@@ -42,21 +58,21 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="sticky-top"> <thead class="sticky-top">
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 col-xl-1">Date</th> <th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-2">Odometer</th> <th scope="col" class="col-2">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3 col-xl-4">Description</th> <th scope="col" class="col-3 col-xl-4">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">Cost</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">Notes</th> <th scope="col" class="col-3">@translator.Translate(userLanguage, "Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (UpgradeRecord upgradeRecord in Model) @foreach (UpgradeRecord upgradeRecord in Model)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)"> <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 col-xl-1">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2">@upgradeRecord.Mileage</td> <td class="col-2">@upgradeRecord.Mileage</td>
<td class="col-3 col-xl-4">@upgradeRecord.Description</td> <td class="col-3 col-xl-4">@upgradeRecord.Description</td>
<td class="col-2">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</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> <td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
</tr> </tr>
} }

View File

@@ -1,5 +1,12 @@
@model List<UploadedFiles> @using CarCareTracker.Helper
<label>Uploaded Documents</label> @inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<UploadedFiles>
<label>@translator.Translate(userLanguage, "Uploaded Documents")</label>
<ul class="list-group"> <ul class="list-group">
@foreach (UploadedFiles filesUploaded in Model) @foreach (UploadedFiles filesUploaded in Model)
{ {

View File

@@ -1,34 +1,17 @@
@using CarCareTracker.Helper @using CarCareTracker.Helper
@inject IConfigHelper config @inject IConfigHelper config
@inject ITranslationHelper translator
@{ @{
var hideZero = config.GetUserConfig(User).HideZero; var userConfig = config.GetUserConfig(User);
var useMPG = config.GetUserConfig(User).UseMPG; var hideZero = userConfig.HideZero;
var useUKMPG = config.GetUserConfig(User).UseUKMPG; var userLanguage = userConfig.UserLanguage;
var useKwh = Model.VehicleData.IsElectric;
string fuelEconomyUnit;
if (useKwh)
{
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
}
else if (useMPG && useUKMPG)
{
fuelEconomyUnit = "mpg";
}
else if (useUKMPG)
{
fuelEconomyUnit = "l/100mi.";
}
else
{
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
}
} }
@model VehicleHistoryViewModel @model VehicleHistoryViewModel
<div class="vehicleDetailTabContainer"> <div class="vehicleDetailTabContainer">
<div class="row mt-2"> <div class="row mt-2">
<div class="d-flex"> <div class="d-flex">
<img src="/defaults/lubelogger_logo.png" /> <img src="/defaults/lubelogger_logo.png" />
<span class="display-6 ms-5">Vehicle Maintenance Report</span> <span class="display-6 ms-5">@translator.Translate(userLanguage, "Vehicle Maintenance Report")</span>
</div> </div>
</div> </div>
<hr /> <hr />
@@ -44,21 +27,21 @@
<li class="list-group-item"> <li class="list-group-item">
@if (Model.VehicleData.IsElectric) @if (Model.VehicleData.IsElectric)
{ {
<span><i class="bi bi-ev-station"></i> Electric</span> <span><i class="bi bi-ev-station me-2"></i>@translator.Translate(userLanguage, "Electric")</span>
} }
else else
{ {
<span><i class="bi bi-fuel-pump"></i> Gasoline</span> <span><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Gasoline")</span>
} }
</li> </li>
</ul> </ul>
</div> </div>
<div class="col-6"> <div class="col-6">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item">Last Reported Odometer Reading: @Model.Odometer</li> <li class="list-group-item">@($"{translator.Translate(userLanguage, "Last Reported Odometer Reading")}: {Model.Odometer}") </li>
<li class="list-group-item">Average Fuel Economy: @($"{Model.MPG} {fuelEconomyUnit}")</li> <li class="list-group-item">@($"{translator.Translate(userLanguage, "Average Fuel Economy")}: {Model.MPG}") </li>
<li class="list-group-item">Total Spent(excl. fuel): @Model.TotalCost.ToString("C")</li> <li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent(excl. fuel)")}: {Model.TotalCost.ToString("C")}") </li>
<li class="list-group-item">Total Spent on Fuel: @Model.TotalGasCost.ToString("C")</li> <li class="list-group-item">@($"{translator.Translate(userLanguage, "Total Spent on Fuel")}: {Model.TotalGasCost.ToString("C")}") </li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -68,12 +51,12 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2 servicehistorytype">Type</th> <th scope="col" class="col-2 servicehistorytype">@translator.Translate(userLanguage, "Type")</th>
<th scope="col" class="col-1 servicehistorydate">Date</th> <th scope="col" class="col-1 servicehistorydate">@translator.Translate(userLanguage, "Date")</th>
<th scope="col" class="col-1">Odometer</th> <th scope="col" class="col-1">@translator.Translate(userLanguage, "Odometer")</th>
<th scope="col" class="col-3">Description</th> <th scope="col" class="col-3">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-1">Cost</th> <th scope="col" class="col-1">@translator.Translate(userLanguage, "Cost")</th>
<th scope="col" class="col-4">Notes</th> <th scope="col" class="col-4">@translator.Translate(userLanguage, "Notes")</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -83,16 +66,19 @@
<td class="col-2 servicehistorytype"> <td class="col-2 servicehistorytype">
@if (reportData.DataType == ImportMode.ServiceRecord) @if (reportData.DataType == ImportMode.ServiceRecord)
{ {
<span><i class="bi bi-card-checklist me-2"></i>Service</span> <span><i class="bi bi-card-checklist me-2"></i>@translator.Translate(userLanguage, "Service")</span>
} else if (reportData.DataType == ImportMode.RepairRecord) }
else if (reportData.DataType == ImportMode.RepairRecord)
{ {
<span><i class="bi bi-exclamation-octagon me-2"></i>Repair</span> <span><i class="bi bi-exclamation-octagon me-2"></i>@translator.Translate(userLanguage, "Repair")</span>
} else if (reportData.DataType == ImportMode.UpgradeRecord) }
else if (reportData.DataType == ImportMode.UpgradeRecord)
{ {
<span><i class="bi bi-wrench-adjustable me-2"></i>Upgrade</span> <span><i class="bi bi-wrench-adjustable me-2"></i>@translator.Translate(userLanguage, "Upgrade")</span>
} else if (reportData.DataType == ImportMode.TaxRecord) }
else if (reportData.DataType == ImportMode.TaxRecord)
{ {
<span><i class="bi bi-currency-dollar me-2"></i>Tax</span> <span><i class="bi bi-currency-dollar me-2"></i>@translator.Translate(userLanguage, "Tax")</span>
} }
</td> </td>
<td class="col-1 servicehistorydate">@reportData.Date.ToShortDateString()</td> <td class="col-1 servicehistorydate">@reportData.Date.ToShortDateString()</td>

View File

@@ -1,5 +1,10 @@
@model Vehicle @using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model Vehicle
@{ @{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var isNew = Model.Id == 0; var isNew = Model.Id == 0;
if (Model.ImageLocation == "/defaults/noimage.png") if (Model.ImageLocation == "/defaults/noimage.png")
{ {
@@ -7,7 +12,7 @@
} }
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="addVehicleModalLabel">@(isNew ? "Add New Vehicle" : "Edit Vehicle")</h5> <h5 class="modal-title" id="addVehicleModalLabel">@(isNew ? translator.Translate(userLanguage, "Add New Vehicle") : translator.Translate(userLanguage, "Edit Vehicle"))</h5>
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn-close" onclick="hideAddVehicleModal()" aria-label="Close"></button> <button type="button" class="btn-close" onclick="hideAddVehicleModal()" aria-label="Close"></button>
@@ -21,25 +26,36 @@
<div class="modal-body"> <div class="modal-body">
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="inputYear">Year</label> <label for="inputYear">@translator.Translate(userLanguage, "Year")</label>
<input type="number" id="inputYear" class="form-control" placeholder="Year(must be after 1900)" value="@(isNew ? "" : Model.Year)"> <input type="number" id="inputYear" class="form-control" placeholder="@translator.Translate(userLanguage, "Year(must be after 1900)")" value="@(isNew ? "" : Model.Year)">
<label for="inputMake">Make</label> <label for="inputMake">@translator.Translate(userLanguage, "Make")</label>
<input type="text" id="inputMake" class="form-control" placeholder="Make" value="@Model.Make"> <input type="text" id="inputMake" class="form-control" placeholder="@translator.Translate(userLanguage, "Make")" value="@Model.Make">
<label for="inputModel">Model</label> <label for="inputModel">@translator.Translate(userLanguage, "Model")</label>
<input type="text" id="inputModel" class="form-control" placeholder="Model" value="@Model.Model"> <input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model">
<label for="inputLicensePlate">License Plate</label> <label for="inputLicensePlate">@translator.Translate(userLanguage, "License Plate")</label>
<input type="text" id="inputLicensePlate" class="form-control" placeholder="License Plate" value="@Model.LicensePlate"> <input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric"> <input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
<label class="form-check-label" for="inputIsElectric">Electric Vehicle</label> <label class="form-check-label" for="inputIsElectric">@translator.Translate(userLanguage, "Electric Vehicle")</label>
</div> </div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputUseHours" checked="@Model.UseHours">
<label class="form-check-label" for="inputUseHours">@translator.Translate(userLanguage, "Use Engine Hours")</label>
</div>
<label for="inputTag">@translator.Translate(userLanguage, "Tags(optional)")</label>
<select multiple class="form-select" id="inputTag">
@foreach (string tag in Model.Tags)
{
<!option value="@tag">@tag</!option>
}
</select>
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation)) @if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
{ {
<label for="inputImage">Replace picture(optional)</label> <label for="inputImage">@translator.Translate(userLanguage, "Replace picture(optional)")</label>
<input onChange="uploadFileAsync(this)" type="file" accept=".png,.jpg,.jpeg" class="form-control-file" id="inputImage"> <input onChange="uploadFileAsync(this)" type="file" accept=".png,.jpg,.jpeg" class="form-control-file" id="inputImage">
} else } else
{ {
<label for="inputImage">Upload a picture(optional)</label> <label for="inputImage">@translator.Translate(userLanguage, "Upload a picture(optional)")</label>
<input onChange="uploadFileAsync(this)" type="file" accept=".png,.jpg,.jpeg" class="form-control-file" id="inputImage"> <input onChange="uploadFileAsync(this)" type="file" accept=".png,.jpg,.jpeg" class="form-control-file" id="inputImage">
} }
</div> </div>
@@ -48,12 +64,12 @@
<div class="modal-footer"> <div class="modal-footer">
@if (isNew) @if (isNew)
{ {
<button type="button" class="btn btn-secondary" onclick="hideAddVehicleModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideAddVehicleModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="saveVehicle(false)" class="btn btn-primary">Add New Vehicle</button> <button type="button" onclick="saveVehicle(false)" class="btn btn-primary">@translator.Translate(userLanguage, "Add New Vehicle")</button>
} else if (!isNew) } else if (!isNew)
{ {
<button type="button" class="btn btn-secondary" onclick="hideEditVehicleModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideEditVehicleModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="saveVehicle(true)" class="btn btn-primary">Save Vehicle</button> <button type="button" onclick="saveVehicle(true)" class="btn btn-primary">@translator.Translate(userLanguage, "Save Vehicle")</button>
} }
</div> </div>
<script> <script>

View File

@@ -16,6 +16,10 @@
"EnableAutoOdometerInsert": false, "EnableAutoOdometerInsert": false,
"UseUKMPG": false, "UseUKMPG": false,
"UseThreeDecimalGasCost": true, "UseThreeDecimalGasCost": true,
"UseMarkDownOnSavedNotes": false,
"PreferredGasMileageUnit": "",
"PreferredGasUnit": "",
"UserLanguage": "en_US",
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ], "VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
"DefaultTab": 8, "DefaultTab": 8,
"UserNameHash": "", "UserNameHash": "",

View File

@@ -10,6 +10,7 @@ services:
volumes: volumes:
- config:/App/config - config:/App/config
- data:/App/data - data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents - documents:/App/wwwroot/documents
- images:/App/wwwroot/images - images:/App/wwwroot/images
- temp:/App/wwwroot/temp - temp:/App/wwwroot/temp
@@ -41,6 +42,7 @@ services:
volumes: volumes:
config: config:
data: data:
translations:
documents: documents:
images: images:
temp: temp:

View File

@@ -10,6 +10,7 @@ services:
volumes: volumes:
- config:/App/config - config:/App/config
- data:/App/data - data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents - documents:/App/wwwroot/documents
- images:/App/wwwroot/images - images:/App/wwwroot/images
- temp:/App/wwwroot/temp - temp:/App/wwwroot/temp
@@ -24,6 +25,7 @@ services:
volumes: volumes:
config: config:
data: data:
translations:
documents: documents:
images: images:
temp: temp:

BIN
docs/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,21 @@
# Sharing your Vehicles and Collaborating with Other Users
LubeLogger allows you to collaborate on vehicles so that more than one user can add or edit records on a vehicle.
To share a vehicle, simply navigate into the Vehicle's Dashboard, and look to the bottom left
![image](https://github.com/hargata/lubelog/assets/155338622/76eda35f-5547-47d9-91cb-3fbf30d2a3d9)
Click on the little blue button with a User Add icon and you will be prompted to enter the user's username
![image](https://github.com/hargata/lubelog/assets/155338622/6bbcc309-a790-48e0-91fa-24c57df20531)
**Note:** The username is case sensitive and the user must exist in the system.
Once you have added the user, their username will then show up in the list of Collaborators
![image](https://github.com/hargata/lubelog/assets/155338622/edac8c3b-eb3e-411d-ab45-e54cb66206f3)
Now the user can view and edit the vehicle as well:
![image](https://github.com/hargata/lubelog/assets/155338622/85b0aacf-ae5c-4361-8d1c-32f9ec637a5b)

View File

@@ -0,0 +1,51 @@
# Configuring LubeLogger
In order to provide the best possible user experience, we have provided ample amount of flexibility when it comes to user settings.
Upon initial launch, you are using the Root User by default without any authentication, so you will have access to all of the settings.
![image](https://github.com/hargata/lubelog/assets/155338622/cf73ff68-e79b-468d-84fb-375e481cbadd)
Most of the settings are relatively straightforward and self-explanatory.
**Note:** If you are a user in the UK and you wish be able to input Fuel Purchases in Liters but display Fuel Mileage as Miles Per UK Gallons, you will need to enable "Use Imperial Calculation" and "Use UK MPG Calculation"
**Note:** When making changes to Settings as a root user, your settings will be saved and served up as the default server settings for any new users that sign up.
## Enable Authentication
It is highly recommended that you secure your LubeLogger instance by enabling authentication.
To do so, simply check "Enable Authentication" and you will be prompted to enter a Username and Password
![image](https://github.com/hargata/lubelog/assets/155338622/4d8b1855-e437-4ade-999b-3dd6ea19e55f)
The credentials that you set up here are the credentials for the Root User, aka the Super Admin, and shouldn't be shared with anyone else.
Once you have entered the credentials, you will then be redirected to a Login page
![image](https://github.com/hargata/lubelog/assets/155338622/26181116-5a03-48cf-972d-89a4b1050cce)
Simply enter the credentials you have just set up and you will be logged right in
![image](https://github.com/hargata/lubelog/assets/155338622/20d9a3b9-80de-4a32-8cb6-1100dd237dbd)
## Setting Up Multiple Users
To set up multiple users, all you have to do is click on the dropdown that has your username on it and select "Admin Panel"
![image](https://github.com/hargata/lubelog/assets/155338622/75f32408-b9f3-4e2c-bec6-ea1c196c3438)
If you have SMTP configured correctly, the "Auto Notify(via Email) switch will be enabled and checked, otherwise it will be disabled/grayed out.
Without SMTP Configured:
![image](https://github.com/hargata/lubelog/assets/155338622/9d75ab55-afb7-4d3e-b830-3191de23695c)
With SMTP Configured:
![image](https://github.com/hargata/lubelog/assets/155338622/d9995c30-dd47-44d1-a73d-39f0f129450b)
To generate a new user token, simply click on the "Generate User Token" button and you will be prompted with the user's email address
![image](https://github.com/hargata/lubelog/assets/155338622/f870d5cd-4d4b-480c-94ea-b1161757793f)
If you have SMTP Configured, the user will then receive an Email that looks similar to this:
![image](https://github.com/hargata/lubelog/assets/155338622/0d5c1adf-43d1-45a5-ad59-91fb3b1476af)
The user can then proceed to Register for an account at your instance of LubeLogger.

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