Compare commits

..

145 Commits
v1.4.3 ... main

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,6 +92,26 @@ namespace CarCareTracker.Controllers
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
[HttpGet]
[Route("/api/whoami")]
public IActionResult WhoAmI()
{
var result = new UserExportModel
{
Username = User.FindFirstValue(ClaimTypes.Name),
EmailAddress = User.IsInRole(nameof(UserData.IsRootUser)) ? _config.GetUserConfig(User).DefaultReminderEmail : User.FindFirstValue(ClaimTypes.Email),
IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)).ToString(),
IsRoot = User.IsInRole(nameof(UserData.IsRootUser)).ToString()
};
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result);
}
}
[HttpGet]
[Route("/api/vehicles")]
public IActionResult Vehicles()
{
@@ -100,7 +120,14 @@ namespace CarCareTracker.Controllers
{
result = _userLogic.FilterUserVehicles(result, GetUserID());
}
return Json(result);
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result);
}
}
[HttpGet]
@@ -153,6 +180,227 @@ namespace CarCareTracker.Controllers
return Json(convertedOdometer);
}
}
#region PlanRecord
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/planrecords")]
public IActionResult PlanRecords(int vehicleId)
{
if (vehicleId == default)
{
var response = OperationResponse.Failed("Must provide a valid vehicle id");
Response.StatusCode = 400;
return Json(response);
}
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new PlanRecordExportModel {
Id = x.Id.ToString(),
DateCreated = x.DateCreated.ToShortDateString(),
DateModified = x.DateModified.ToShortDateString(),
Description = x.Description,
Cost = x.Cost.ToString(),
Notes = x.Notes,
Type = x.ImportMode.ToString(),
Priority = x.Priority.ToString(),
Progress = x.Progress.ToString(),
ExtraFields = x.ExtraFields,
Files = x.Files });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(result, StaticHelper.GetInvariantOption());
}
else
{
return Json(result);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
[Consumes("application/json")]
public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input);
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
public IActionResult AddPlanRecord(int vehicleId, PlanRecordExportModel input)
{
if (vehicleId == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Must provide a valid vehicle id"));
}
if (string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Cost) ||
string.IsNullOrWhiteSpace(input.Type) ||
string.IsNullOrWhiteSpace(input.Priority) ||
string.IsNullOrWhiteSpace(input.Progress))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Description, Cost, Type, Priority, and Progress cannot be empty."));
}
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
if (!validType || !validPriority || !validProgress)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
}
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
}
if (parsedProgress == PlanProgress.Done)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
//hardening - turns null values for List types into empty lists.
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var planRecord = new PlanRecord()
{
VehicleId = vehicleId,
DateCreated = DateTime.Now,
DateModified = DateTime.Now,
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost),
ImportMode = parsedType,
Priority = parsedPriority,
Progress = parsedProgress,
ExtraFields = input.ExtraFields,
Files = input.Files
};
_planRecordDataAccess.SavePlanRecordToVehicle(planRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord, "planrecord.add.api", User.Identity.Name));
return Json(OperationResponse.Succeed("Plan Record Added"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpDelete]
[Route("/api/vehicle/planrecords/delete")]
public IActionResult DeletePlanRecord(int id)
{
var existingRecord = _planRecordDataAccess.GetPlanRecordById(id);
if (existingRecord == null || existingRecord.Id == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
//restore any requisitioned supplies.
if (existingRecord.RequisitionHistory.Any())
{
_vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description);
}
var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete.api", User.Identity.Name));
}
return Json(OperationResponse.Conditional(result, "Plan Record Deleted"));
}
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
[Consumes("application/json")]
public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input);
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
public IActionResult UpdatePlanRecord(PlanRecordExportModel input)
{
if (string.IsNullOrWhiteSpace(input.Id) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Cost) ||
string.IsNullOrWhiteSpace(input.Type) ||
string.IsNullOrWhiteSpace(input.Priority) ||
string.IsNullOrWhiteSpace(input.Progress))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Description, Cost, Type, Priority, and Progress cannot be empty."));
}
bool validType = Enum.TryParse(input.Type, out ImportMode parsedType);
bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority);
bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress);
if (!validType || !validPriority || !validProgress)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid."));
}
if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord"));
}
if (parsedProgress == PlanProgress.Done)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
var existingRecord = _planRecordDataAccess.GetPlanRecordById(int.Parse(input.Id));
if (existingRecord != null && existingRecord.Id == int.Parse(input.Id))
{
//check if user has access to the vehicleId
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
existingRecord.DateModified = DateTime.Now;
existingRecord.Description = input.Description;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Cost = decimal.Parse(input.Cost);
existingRecord.ImportMode = parsedType;
existingRecord.Priority = parsedPriority;
existingRecord.Progress = parsedProgress;
existingRecord.Files = input.Files;
existingRecord.ExtraFields = input.ExtraFields;
_planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.update.api", User.Identity.Name));
}
else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
return Json(OperationResponse.Succeed("Plan Record Updated"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
#endregion
#region ServiceRecord
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
@@ -198,6 +446,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var serviceRecord = new ServiceRecord()
@@ -278,6 +534,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -360,6 +624,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var repairRecord = new CollisionRecord()
@@ -441,6 +713,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -524,6 +804,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var upgradeRecord = new UpgradeRecord()
@@ -604,6 +892,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -720,6 +1016,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var taxRecord = new TaxRecord()
@@ -783,6 +1087,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -882,6 +1194,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var odometerRecord = new OdometerRecord()
@@ -943,6 +1263,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -1042,6 +1370,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var gasRecord = new GasRecord()
@@ -1121,6 +1457,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -1160,6 +1504,7 @@ namespace CarCareTracker.Controllers
}
}
#endregion
#region ReminderRecord
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/reminders")]
@@ -1172,7 +1517,7 @@ namespace CarCareTracker.Controllers
}
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString()});
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(results, StaticHelper.GetInvariantOption());
@@ -1182,6 +1527,183 @@ namespace CarCareTracker.Controllers
return Json(results);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
[Consumes("application/json")]
public IActionResult AddReminderRecordJson(int vehicleId, [FromBody] ReminderExportModel input) => AddReminderRecord(vehicleId, input);
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
public IActionResult AddReminderRecord(int vehicleId, ReminderExportModel input)
{
if (vehicleId == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Must provide a valid vehicle id"));
}
if (string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
var reminderRecord = new ReminderRecord()
{
VehicleId = vehicleId,
Description = input.Description,
Mileage = parsedOdometer,
Date = parsedDate,
Metric = parsedMetric,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
};
_reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord, "reminderrecord.add.api", User.Identity.Name));
return Json(OperationResponse.Succeed("Reminder Record Added"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpPut]
[Route("/api/vehicle/reminders/update")]
[Consumes("application/json")]
public IActionResult UpdateReminderRecordJson([FromBody] ReminderExportModel input) => UpdateReminderRecord(input);
[HttpPut]
[Route("/api/vehicle/reminders/update")]
public IActionResult UpdateReminderRecord(ReminderExportModel input)
{
if (string.IsNullOrWhiteSpace(input.Id) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
//retrieve existing record
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(int.Parse(input.Id));
if (existingRecord != null && existingRecord.Id == int.Parse(input.Id))
{
//check if user has access to the vehicleId
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
existingRecord.Date = parsedDate;
existingRecord.Mileage = parsedOdometer;
existingRecord.Description = input.Description;
existingRecord.Metric = parsedMetric;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.update.api", User.Identity.Name));
}
else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
return Json(OperationResponse.Succeed("Reminder Record Updated"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpDelete]
[Route("/api/vehicle/reminders/delete")]
public IActionResult DeleteReminderRecord(int id)
{
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(id);
if (existingRecord == null || existingRecord.Id == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete.api", User.Identity.Name));
}
return Json(OperationResponse.Conditional(result, "Reminder Record Deleted"));
}
[HttpGet]
[Route("/api/calendar")]
public IActionResult Calendar()
@@ -1195,6 +1717,7 @@ namespace CarCareTracker.Controllers
var calendarContent = StaticHelper.RemindersToCalendar(reminders);
return File(calendarContent, "text/calendar");
}
#endregion
[HttpPost]
[Route("/api/documents/upload")]
public IActionResult UploadDocument(List<IFormFile> documents)
@@ -1232,6 +1755,11 @@ namespace CarCareTracker.Controllers
[Route("/api/vehicle/reminders/send")]
public IActionResult SendReminders(List<ReminderUrgency> urgencies)
{
if (!urgencies.Any())
{
//if no urgencies parameter, we will default to all urgencies.
urgencies = new List<ReminderUrgency> { ReminderUrgency.NotUrgent, ReminderUrgency.Urgent, ReminderUrgency.VeryUrgent, ReminderUrgency.PastDue };
}
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -196,7 +196,7 @@ namespace CarCareTracker.Controllers
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var userConfig = _config.GetUserConfig(User);
var totalDistanceTraveled = maxMileage - minMileage;
var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, serviceRecords, collisionRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords);
var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, year, serviceRecords, collisionRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords);
var viewModel = new CostTableForVehicle
{
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
@@ -473,7 +473,8 @@ namespace CarCareTracker.Controllers
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.ServiceRecord,
ExtraFields = x.ExtraFields
ExtraFields = x.ExtraFields,
RequisitionHistory = x.RequisitionHistory
}));
//repair records
reportData.AddRange(vehicleRecords.CollisionRecords.Select(x => new GenericReportModel
@@ -484,7 +485,8 @@ namespace CarCareTracker.Controllers
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.RepairRecord,
ExtraFields = x.ExtraFields
ExtraFields = x.ExtraFields,
RequisitionHistory = x.RequisitionHistory
}));
reportData.AddRange(vehicleRecords.UpgradeRecords.Select(x => new GenericReportModel
{
@@ -494,7 +496,8 @@ namespace CarCareTracker.Controllers
Notes = x.Notes,
Cost = x.Cost,
DataType = ImportMode.UpgradeRecord,
ExtraFields = x.ExtraFields
ExtraFields = x.ExtraFields,
RequisitionHistory = x.RequisitionHistory
}));
reportData.AddRange(vehicleRecords.TaxRecords.Select(x => new GenericReportModel
{

View File

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

View File

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

12
Enum/ExtraFieldType.cs Normal file
View File

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

View File

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

View File

@@ -24,8 +24,10 @@ namespace CarCareTracker.Helper
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
string GetServerDomain();
bool DeleteUserConfig(int userId);
bool GetInvariantApi();
bool GetServerOpenRegistration();
}
public class ConfigHelper : IConfigHelper
{
@@ -61,6 +63,15 @@ namespace CarCareTracker.Helper
var motd = CheckString("LUBELOGGER_MOTD");
return motd;
}
public string GetServerDomain()
{
var domain = CheckString("LUBELOGGER_DOMAIN");
return domain;
}
public bool GetServerOpenRegistration()
{
return CheckBool(CheckString("LUBELOGGER_OPEN_REGISTRATION"));
}
public OpenIDConfig GetOpenIDConfig()
{
OpenIDConfig openIdConfig = _config.GetSection("OpenIDConfig").Get<OpenIDConfig>() ?? new OpenIDConfig();
@@ -233,6 +244,7 @@ namespace CarCareTracker.Helper
UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"),
HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))),
EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))),
ShowCalendar = CheckBool(CheckString(nameof(UserConfig.ShowCalendar))),
EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))),
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>() ?? new UserConfig().VisibleTabs,
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>() ?? new UserConfig().TabOrder,

View File

@@ -6,6 +6,7 @@ namespace CarCareTracker.Helper
public interface IFileHelper
{
string GetFullFilePath(string currentFilePath, bool mustExist = true);
byte[] GetFileBytes(string fullFilePath, bool deleteFile = false);
string MoveFileFromTemp(string currentFilePath, string newFolder);
bool RenameFile(string currentFilePath, string newName);
bool DeleteFile(string currentFilePath);
@@ -85,6 +86,19 @@ namespace CarCareTracker.Helper
return string.Empty;
}
}
public byte[] GetFileBytes(string fullFilePath, bool deleteFile = false)
{
if (File.Exists(fullFilePath))
{
var fileBytes = File.ReadAllBytes(fullFilePath);
if (deleteFile)
{
File.Delete(fullFilePath);
}
return fileBytes;
}
return Array.Empty<byte>();
}
public bool RestoreBackup(string fileName, bool clearExisting = false)
{
var fullFilePath = GetFullFilePath(fileName);

View File

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

View File

@@ -25,7 +25,14 @@ namespace CarCareTracker.Helper
existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval);
} else
{
existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval);
if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months)
{
existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval);
}
else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days)
{
existingReminder.Date = newDate.Date.AddDays(existingReminder.CustomMonthInterval);
}
}
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
@@ -55,7 +62,14 @@ namespace CarCareTracker.Helper
}
else
{
existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval);
if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months)
{
existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval);
}
else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days)
{
existingReminder.Date = newDate.AddDays(existingReminder.CustomMonthInterval);
}
}
}
return existingReminder;

View File

@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public const string VersionNumber = "1.4.3";
public const string VersionNumber = "1.4.7";
public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "data/config/userConfig.json";
public const string LegacyUserConfigPath = "config/userConfig.json";
@@ -262,7 +262,9 @@ namespace CarCareTracker.Helper
//update isrequired setting
foreach (ExtraField extraField in recordExtraFields)
{
extraField.IsRequired = templateExtraFields.Where(x => x.Name == extraField.Name).First().IsRequired;
var firstMatchingField = templateExtraFields.First(x => x.Name == extraField.Name);
extraField.IsRequired = firstMatchingField.IsRequired;
extraField.FieldType = firstMatchingField.FieldType;
}
//append extra fields
foreach (ExtraField extraField in templateExtraFields)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,8 @@ namespace CarCareTracker.Models
}
public class ReminderExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
public string Description { get; set; }
public string Urgency { get; set; }
public string Metric { get; set; }
@@ -123,17 +125,33 @@ namespace CarCareTracker.Models
public string DueDate { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string DueOdometer { get; set; }
public string Tags { get; set; }
}
public class PlanRecordExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateCreated { get; set; }
[JsonConverter(typeof(FromDateOptional))]
public string DateModified { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public string Type { get; set; }
public string Priority { get; set; }
public string Progress { get; set; }
[JsonConverter(typeof(FromDecimalOptional))]
public string Cost { get; set; }
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
public class UserExportModel
{
public string Username { get; set; }
public string EmailAddress { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string IsAdmin { get; set; }
[JsonConverter(typeof(FromBoolOptional))]
public string IsRoot { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,8 +69,8 @@ builder.Services.AddSingleton<IGasHelper, GasHelper>();
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
builder.Services.AddSingleton<IConfigHelper, ConfigHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<ITranslationHelper, TranslationHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
//configure logic
builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
@@ -116,7 +116,7 @@ app.UseStaticFiles(new StaticFileOptions
{
if (ctx.Context.Request.Path.StartsWithSegments("/images"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
ctx.Context.Response.Headers.Append("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
ctx.Context.Response.Redirect("/Login");
@@ -133,7 +133,7 @@ app.UseStaticFiles(new StaticFileOptions
{
if (ctx.Context.Request.Path.StartsWithSegments("/documents"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
ctx.Context.Response.Headers.Append("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
ctx.Context.Response.Redirect("/Login");
@@ -156,7 +156,7 @@ app.UseStaticFiles(new StaticFileOptions
{
if (ctx.Context.Request.Path.StartsWithSegments("/temp"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
ctx.Context.Response.Headers.Append("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
ctx.Context.Response.Redirect("/Login");

View File

@@ -30,7 +30,21 @@
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/whoami</code>
</div>
<div class="col-3">
Returns information for current user
</div>
<div class="col-3">
No Params
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/vehicles</code>
</div>
<div class="col-3">
@@ -44,7 +58,7 @@
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/vehicle/info</code>
</div>
<div class="col-3">
@@ -159,6 +173,83 @@
Id - Id of Odometer Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords</code>
</div>
<div class="col-3">
Returns a list of plan records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/add</code>
</div>
<div class="col-3">
Adds Plan Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
description - Description<br />
cost - Cost<br />
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
priority - Low/Normal/Critical<br />
progress - Backlog/InProgress/Testing<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/update</code>
</div>
<div class="col-3">
Updates Plan Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Plan Record<br />
description - Description<br />
cost - Cost<br />
type - ServiceRecord/RepairRecord/UpgradeRecord<br />
priority - Low/Normal/Critical<br />
progress - Backlog/InProgress/Testing<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
files - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showAttachmentsInfo()">attachments(optional)</a><br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/planrecords/delete</code>
</div>
<div class="col-3">
Deletes Plan Record
</div>
<div class="col-3">
Id - Id of Plan Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
@@ -408,7 +499,7 @@
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/vehicle/taxrecords/check</code>
</div>
<div class="col-3">
@@ -578,6 +669,79 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/add</code>
</div>
<div class="col-3">
Adds Reminder Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/update</code>
</div>
<div class="col-3">
Updates Reminder Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Reminder Record<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/delete</code>
</div>
<div class="col-3">
Deletes Reminder Record
</div>
<div class="col-3">
Id - Id of Reminder Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/calendar</code>
</div>
<div class="col-3">
Returns reminder calendar in ICS format
</div>
<div class="col-3">
No Params
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
@@ -600,7 +764,7 @@
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/vehicle/reminders/send</code>
</div>
<div class="col-3">
@@ -608,14 +772,14 @@
</div>
<div class="col-3">
(must be root user)<br />
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue](optional)
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/makebackup</code>
</div>
<div class="col-3">
@@ -629,7 +793,7 @@
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable">
<div class="col-5 copyable testable">
<code>/api/cleanup</code>
</div>
<div class="col-3">
@@ -637,13 +801,20 @@
</div>
<div class="col-3">
(must be root user)<br />
deepClean(bool) - Perform deep clean
deepClean(bool) - Perform deep clean(optional)
</div>
</div>
}
<script>
$('.copyable').on('click', function (e) {
copyToClipboard(e.currentTarget);
if (e.ctrlKey || e.metaKey){
let targetElement = $(e.currentTarget);
if (targetElement.hasClass("testable")){
window.location = targetElement.text().trim();
}
} else {
copyToClipboard(e.currentTarget);
}
})
function showExtraFieldsInfo(){
Swal.fire({

View File

@@ -27,13 +27,15 @@
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
@if(userConfig.ShowCalendar){
<li class="nav-item" role="presentation">
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</span></button>
</li>
@if (User.IsInRole("CookieAuth"))
@if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth"))
{
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
@@ -78,13 +80,15 @@
<button class="nav-link resizable-nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
@if (userConfig.ShowCalendar){
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
}
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link resizable-nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Settings")</span></button>
</li>
@if (User.IsInRole("CookieAuth"))
@if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth"))
{
<li class="nav-item dropdown" role="presentation">
<a class="nav-link resizable-nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person"></i><span class="ms-2 d-sm-none d-md-inline">@User.Identity.Name</span></a>
@@ -146,6 +150,8 @@
</div>
</div>
</div>
<div class="stickerPrintContainer hideOnPrint">
</div>
<script>
loadGarage();
bindWindowResize();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -158,6 +158,8 @@
<div class="modal-content" id="inputSuppliesModalContent"></div>
</div>
</div>
<div class="stickerPrintContainer hideOnPrint">
</div>
<script>
function GetVehicleId() {
return {

View File

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

View File

@@ -21,30 +21,7 @@
<div class="alert alert-danger" role="alert">
@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>
@if (Model == ImportMode.GasRecord)
{
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
{
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.TaxRecord)
{
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.SupplyRecord)
{
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.PlanRecord)
{
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
else if (Model == ImportMode.OdometerRecord)
{
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
}
<a class="btn btn-link" href="@($"/Vehicle/GenerateCsvSample?mode={Model.ToString()}")" target="_blank">@translator.Translate(userLanguage, "Download Sample")</a>
</div>
</div>
<div class="row mt-2">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,7 +168,6 @@
</div>
</div>
}
<div id="vehicleHistoryReport" class="showOnPrint"></div>
<script>
getSelectedMetrics();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
var userLanguage = userConfig.UserLanguage;
var extraFields = Model.ReportParameters.ExtraFields;
}
<div class="vehicleDetailTabContainer">
<div style="page-break-after: always;">
<div class="row mt-2">
<div class="d-flex">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
@@ -18,13 +18,12 @@
</span>
@if (!string.IsNullOrWhiteSpace(Model.StartDate) && !string.IsNullOrWhiteSpace(Model.EndDate))
{
<br />
<span class="lead ms-2">
@($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}")
</span>
<br />
<span class="lead ms-2">
@($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}")
</span>
}
</div>
</div>
</div>
<hr />
@@ -93,26 +92,26 @@
<hr />
@if (Model.TotalDepreciation != default)
{
<div class="row">
<div class="col-3">
@(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation"))
</div>
<div class="col-3">
<span><i class="bi @(Model.TotalDepreciation > 0 ? "bi-graph-down-arrow" : "bi-graph-up-arrow") me-2"></i>@Math.Abs(Model.TotalDepreciation).ToString("C")</span>
</div>
@if (Model.DepreciationPerDay != default)
{
<div class="col-3">
<span><i class="bi bi-calendar-event me-2"></i>@($"{Model.DepreciationPerDay.ToString("C")}/{translator.Translate(userLanguage, "day")}")</span>
</div>
}
@if (Model.DepreciationPerMile != default)
{
<div class="col-3">
<span><i class="bi bi-speedometer me-2"></i>@($"{Model.DepreciationPerMile.ToString("C")}/{Model.DistanceUnit}")</span>
</div>
}
<div class="row">
<div class="col-3">
@(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation"))
</div>
<div class="col-3">
<span><i class="bi @(Model.TotalDepreciation > 0 ? "bi-graph-down-arrow" : "bi-graph-up-arrow") me-2"></i>@Math.Abs(Model.TotalDepreciation).ToString("C")</span>
</div>
@if (Model.DepreciationPerDay != default)
{
<div class="col-3">
<span><i class="bi bi-calendar-event me-2"></i>@($"{Model.DepreciationPerDay.ToString("C")}/{translator.Translate(userLanguage, "day")}")</span>
</div>
}
@if (Model.DepreciationPerMile != default)
{
<div class="col-3">
<span><i class="bi bi-speedometer me-2"></i>@($"{Model.DepreciationPerMile.ToString("C")}/{Model.DistanceUnit}")</span>
</div>
}
</div>
<hr />
}
<div class="row">
@@ -177,3 +176,113 @@
</div>
</div>
</div>
@if (Model.ReportParameters.PrintIndividualRecords){
@foreach (GenericReportModel genericRecord in Model.VehicleHistory)
{
<div class="d-flex flex-column recordSticker">
<div class="d-flex">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
</div>
<hr />
<div class="row">
<div class="col-6">
<ul class="list-group">
<li class="list-group-item">
<span class="display-6">@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")</span>
</li>
<li class="list-group-item">
<span class="lead">@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")</span>
</li>
@foreach (ExtraField extraField in Model.VehicleData.ExtraFields)
{
if (!string.IsNullOrWhiteSpace(extraField.Value))
{
<li class="list-group-item">
<span class="lead">@($"{extraField.Name}: {extraField.Value}")</span>
</li>
}
}
</ul>
</div>
<div class="col-6">
<ul class="list-group">
@if (!string.IsNullOrWhiteSpace(genericRecord.Description))
{
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Description")}: {genericRecord.Description}")
</li>
}
@switch (genericRecord.DataType)
{
case ImportMode.ServiceRecord:
case ImportMode.RepairRecord:
case ImportMode.UpgradeRecord:
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Odometer}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
</li>
break;
case ImportMode.TaxRecord:
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}")
</li>
<li class="list-group-item">
@($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}")
</li>
break;
}
@foreach (ExtraField extraField in genericRecord.ExtraFields)
{
<li class="list-group-item">
@($"{extraField.Name}: {extraField.Value}")
</li>
}
</ul>
</div>
</div>
<hr />
@if (genericRecord.RequisitionHistory.Any())
{
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part Number")</th>
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Description")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Quantity")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Cost")</th>
</tr>
</thead>
<tbody>
@foreach (SupplyUsageHistory usageHistory in genericRecord.RequisitionHistory)
{
<tr class="d-flex">
<td class="col-2 text-truncate">@usageHistory.PartNumber</td>
<td class="col-6 text-truncate">@usageHistory.Description</td>
<td class="col-2">@usageHistory.Quantity.ToString("F")</td>
<td class="col-2">@usageHistory.Cost.ToString("C2")</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<hr />
}
<div class="row flex-grow-1 flex-shrink-1">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(genericRecord.Notes)
</div>
</div>
</div>
</div>
}
<script>setMarkDownStickerNotes()</script>
}

View File

@@ -36,14 +36,7 @@
<input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model">
<label for="inputLicensePlate">@translator.Translate(userLanguage, "License Plate")</label>
<input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
@foreach (ExtraField field in Model.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
<label for="inputIdentifier" class="@(!Model.ExtraFields.Any() ? "d-none" : "")">@translator.Translate(userLanguage, "Identifier")</label>
<select class="form-select @(!Model.ExtraFields.Any() ? "d-none" : "")" id="inputIdentifier" )>
<!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option>
@@ -89,9 +82,15 @@
</div>
<div id="collapsePurchaseInfo" class="accordion-collapse collapse" data-bs-parent="#vehicleModalAccordion">
<label for="inputPurchaseDate">@translator.Translate(userLanguage, "Purchased Date(optional)")</label>
<input type="text" id="inputPurchaseDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Date")" value="@Model.PurchaseDate">
<div class="input-group">
<input type="text" id="inputPurchaseDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Date")" value="@Model.PurchaseDate">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="inputSoldDate">@translator.Translate(userLanguage, "Sold Date(optional)")</label>
<input type="text" id="inputSoldDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Sold Date")" value="@Model.SoldDate">
<div class="input-group">
<input type="text" id="inputSoldDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Sold Date")" value="@Model.SoldDate">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="inputPurchasePrice">@translator.Translate(userLanguage, "Purchased Price(optional)")</label>
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="inputPurchasePrice" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Price")" value="@(Model.PurchasePrice == default ? "" : Model.PurchasePrice)">
<label for="inputSoldPrice">@translator.Translate(userLanguage, "Sold Price(optional)")</label>

View File

@@ -19,6 +19,7 @@
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
"EnableShopSupplies": false,
"ShowCalendar": true,
"EnableExtraFieldColumns": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,

View File

@@ -8,13 +8,7 @@ services:
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- temp:/App/wwwroot/temp
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
@@ -35,12 +29,6 @@ services:
- /etc/localtime:/etc/localtime:ro
volumes:
config:
data:
translations:
documents:
images:
temp:
log:
keys:
postgres:

View File

@@ -8,13 +8,7 @@ services:
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- temp:/App/wwwroot/temp
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
@@ -40,13 +34,7 @@ services:
traefik.http.services.whoami.loadbalancer.server.port: 5000
volumes:
config:
data:
translations:
documents:
images:
temp:
log:
keys:
networks:

View File

@@ -8,13 +8,7 @@ services:
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- translations:/App/wwwroot/translations
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- temp:/App/wwwroot/temp
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
@@ -23,11 +17,5 @@ services:
- .env
volumes:
config:
data:
translations:
documents:
images:
temp:
log:
keys:

View File

@@ -2,7 +2,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<title>LubeLogger Configurator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
@@ -112,6 +112,11 @@
<input type="text" id="inputSmtpFrom" class="form-control">
<small class="text-body-secondary">Sender Email Address</small>
</div>
<div class="form-group">
<label for="inputServerDomain">LubeLogger Domain</label>
<input type="text" id="inputServerDomain" class="form-control">
<small class="text-body-secondary">For Links in Emails</small>
</div>
</div>
</div>
</div>
@@ -150,6 +155,11 @@
<input type="text" id="inputOIDCTokenURL" class="form-control">
<small class="text-body-secondary">Token URL from Provider</small>
</div>
<div class="form-group">
<label for="inputOIDCUserInfoURL">User Info URL</label>
<input type="text" id="inputOIDCUserInfoURL" class="form-control">
<small class="text-body-secondary">Required by some Providers</small>
</div>
<div class="form-group">
<label for="inputOIDCRedirectURL">LubeLogger URL</label>
<input type="text" id="inputOIDCRedirectURL" class="form-control">
@@ -227,7 +237,9 @@
<textarea id="outputModalText" readonly style="width:100%; height:450px;"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-strip me-auto" onclick="removeDoubleQuotes()">Remove Double Quotes</button>
<button type="button" class="btn btn-secondary btn-strip me-auto" onclick="removeDoubleQuotes()">Remove Double Quotes</button>
<input id="appSettingsUpload" onChange="readUploadedFile()" class="d-none" type="file" accept="application/json">
<button type="button" class="btn btn-secondary btn-upload me-auto" onclick="uploadAndMerge()">Upload appsettings.json</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-copy" onclick="copyToClipboard()">Copy</button>
</div>
@@ -237,6 +249,44 @@
</div>
</body>
<script>
function uploadAndMerge(){
$("#appSettingsUpload").click();
}
function readUploadedFile(){
let fl_files = $("#appSettingsUpload")[0].files; // JS FileList object
if (fl_files.length == 0) {
return;
}
// use the 1st file from the list
let fl_file = fl_files[0];
let reader = new FileReader(); // built in API
let display_file = ( e ) => { // set the contents of the <textarea>
mergeIntoUploadedFile(e.target.result);
};
let on_reader_load = ( fl ) => {
return display_file; // a function
};
// Closure to capture the file information.
reader.onload = on_reader_load( fl_file );
// Read the file as text.
reader.readAsText( fl_file );
}
function mergeIntoUploadedFile(fileContents){
var newJsonObject = JSON.parse("{" + $("#outputModalText").text() + "}");
var currentJsonObject = JSON.parse(fileContents);
var mergedJsonObject = {...currentJsonObject, ...newJsonObject};
$("#outputModalLabel").text("Content for appsettings.json");
$("#outputModalText").text(JSON.stringify(mergedJsonObject, null, 2));
//clear out uploaded file content
$("#appSettingsUpload").val("");
}
function removeDoubleQuotes(){
var currentText = $("#outputModalText").text();
$("#outputModalText").text(currentText.replaceAll('"', ''));
@@ -276,6 +326,9 @@ function generateConfig(){
if ($("#inputInvariantAPI").is(":checked")){
windowConfig["LUBELOGGER_INVARIANT_API"]=$('#inputInvariantAPI').is(':checked');
}
if ($("#inputServerDomain").val().trim() != ''){
windowConfig["LUBELOGGER_DOMAIN"] = $('#inputServerDomain').val();
}
if ($('#inputSmtpServer').val().trim() != ''){
windowConfig["MailConfig"] = {
EmailServer: $("#inputSmtpServer").val(),
@@ -292,6 +345,7 @@ function generateConfig(){
ClientSecret: $("#inputOIDCClientSecret").val(),
AuthURL: $("#inputOIDCAuthURL").val(),
TokenURL: $("#inputOIDCTokenURL").val(),
UserInfoURL: $("#inputOIDCUserInfoURL").val(),
RedirectURL: redirectUrl,
Scope: $("#inputOIDCScope").val(),
ValidateState: $("#inputOIDCValidateState").is(":checked"),
@@ -319,6 +373,11 @@ function generateConfig(){
$("#outputModalLabel").text("Append into appsettings.json");
$("#outputModalText").text(JSON.stringify(windowConfig, null, 2).slice(1,-1));
$(".btn-strip").hide();
if (jQuery.isEmptyObject(windowConfig)){
$(".btn-upload").hide();
} else {
$(".btn-upload").show();
}
$("#outputModal").modal("show");
} else {
var dockerConfig = [];
@@ -341,6 +400,9 @@ function generateConfig(){
if ($("#inputPostgres").val().trim() != ''){
dockerConfig.push(`POSTGRES_CONNECTION="${$('#inputPostgres').val()}"`);
}
if ($("#inputServerDomain").val().trim() != ''){
dockerConfig.push(`LUBELOGGER_DOMAIN="${$('#inputServerDomain').val()}"`);
}
if ($("#inputCustomWidgets").is(":checked")){
dockerConfig.push(`LUBELOGGER_CUSTOM_WIDGETS="${$('#inputCustomWidgets').is(':checked')}"`);
}
@@ -360,6 +422,7 @@ function generateConfig(){
dockerConfig.push(`OpenIDConfig__ClientSecret="${$('#inputOIDCClientSecret').val()}"`);
dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`);
dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`);
dockerConfig.push(`OpenIDConfig__UserInfoURL="${$('#inputOIDCUserInfoURL').val()}"`);
dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`);
dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`);
dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`);
@@ -375,6 +438,7 @@ function generateConfig(){
$("#outputModalLabel").text("Content for .env");
$("#outputModalText").text(dockerConfig.join("\r\n"));
$(".btn-strip").show();
$(".btn-upload").hide();
$("#outputModal").modal("show");
}
}

8
my-.env Normal file
View File

@@ -0,0 +1,8 @@
LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8
MailConfig__EmailServer="box.cahaa.net"
MailConfig__EmailFrom="lubelog@cahaa.net"
MailConfig__UseSSL="true"
MailConfig__Port=465
MailConfig__Username="clint@cahaa.net"
MailConfig__Password="***************"

28
my-docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
app:
image: ghcr.io/hargata/lubelogger:latest
build: .
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- temp:/App/wwwroot/temp
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
- 18180:8080
env_file:
- .env
volumes:
config: null
data: null
documents: null
images: null
temp: null
log: null
keys: null
networks: {}

View File

@@ -48,12 +48,10 @@ html {
.swimlane{
height:100%;
}
.swimlane.mid {
.swimlane:not(:last-child) {
border-right-style: solid;
}
.swimlane.end {
border-left-style: solid;
}
.showOnPrint {
display: none;
@@ -81,6 +79,34 @@ html {
color: #000 !important;
overflow: visible;
}
.stickerPrintContainer {
background-color: #fff !important;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
margin: 0;
font-size: 14px;
line-height: 18px;
color: #000 !important;
overflow: visible;
z-index: 1030;
}
.reminderSticker {
width: 98%;
aspect-ratio: 1/1;
border-style: dashed;
border-width: 2px;
page-break-after: always;
}
.recordSticker {
height: 100%;
page-break-after: always;
}
.stickerNote {
height:100%;
}
body {
background-color: #fff !important;
@@ -478,6 +504,12 @@ html[data-bs-theme="light"] .api-method:hover {
object-fit: scale-down;
pointer-events: none;
}
.lubelogger-logo-sticker {
height: 6rem;
width: auto;
object-fit: scale-down;
pointer-events: none;
}
::-ms-reveal {
display: none;
@@ -487,3 +519,11 @@ html[data-bs-theme="light"] .api-method:hover {
border-top: thin solid black;
text-align: center;
}
.attachment-badge-xs {
padding-left: 0.25em;
padding-right: 0.25em;
font-size: 0.6em;
font-weight: 500;
top: 15%
}

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