Compare commits

...

34 Commits

Author SHA1 Message Date
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
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
37 changed files with 740 additions and 147 deletions

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

@@ -120,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]

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()
{
@@ -555,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

@@ -136,7 +136,15 @@ namespace CarCareTracker.Controllers
//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
{
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 +188,108 @@ 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 userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_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
{
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)
{

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]

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

@@ -238,6 +238,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

@@ -11,6 +11,7 @@ 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
{
@@ -74,6 +75,28 @@ namespace CarCareTracker.Helper
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)
{
return OperationResponse.Succeed("Email Sent!");
}
else
{
return OperationResponse.Failed();
}
}
public OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))

View File

@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public const string VersionNumber = "1.4.5";
public const string VersionNumber = "1.4.6";
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

@@ -8,7 +8,7 @@
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;

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

@@ -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

@@ -27,9 +27,11 @@
<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>
@@ -78,9 +80,11 @@
<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>

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

@@ -0,0 +1,219 @@
@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="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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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>
@@ -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>

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

@@ -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

@@ -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>

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

@@ -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,

File diff suppressed because one or more lines are too long

View File

@@ -323,10 +323,11 @@ function updateMPGLabels() {
var maxMPG = rowMPG.length > 0 ? rowMPG.reduce((a, b) => a > b ? a : b) : 0;
var minMPG = rowMPG.length > 0 && rowNonZeroMPG.length > 0 ? rowNonZeroMPG.reduce((a, b) => a < b ? a : b) : 0;
var totalMilesTraveled = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
var totalGasConsumed = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
var totalUnaggregatedGasConsumed = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
var totalGasConsumed = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
var totalGasConsumedFV = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
var totalUnaggregatedGasConsumedFV = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
var totalMilesTraveledUnaggregated = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
var fullGasConsumed = totalGasConsumed + totalUnaggregatedGasConsumed;
var fullGasConsumed = totalGasConsumedFV + totalUnaggregatedGasConsumedFV;
var fullDistanceTraveled = totalMilesTraveled + totalMilesTraveledUnaggregated;
if (totalGasConsumed > 0 && rowNonZeroMPG.length > 0) {
var averageMPG = totalMilesTraveled / totalGasConsumed;

View File

@@ -4,6 +4,15 @@
$("#extraFieldModal").modal('show');
});
}
function showServerConfigModal() {
$.get(`/Home/GetServerConfiguration`, function (data) {
$("#serverConfigModalContent").html(data);
$("#serverConfigModal").modal('show');
});
}
function hideServerConfigModal() {
$("#serverConfigModal").modal('hide');
}
function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide');
}
@@ -62,6 +71,7 @@ function updateSettings() {
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
showCalendar: $("#showCalendar").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),
@@ -85,6 +95,33 @@ function updateSettings() {
}
})
}
function sendTestEmail() {
Swal.fire({
title: 'Send Test Email',
html: `
<input type="text" id="testEmailRecipient" class="swal2-input" placeholder="Email Address" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Send',
focusConfirm: false,
preConfirm: () => {
const emailRecipient = $("#testEmailRecipient").val();
if (!emailRecipient || emailRecipient.trim() == '') {
Swal.showValidationMessage(`Please enter a valid email address`);
}
return { emailRecipient }
},
}).then(function (result) {
if (result.isConfirmed) {
$.post('/Home/SendTestEmail', { emailAddress: result.value.emailRecipient }, function (data) {
if (data.success) {
successToast(data.message);
} else {
errorToast(data.message);
}
});
}
});
}
function makeBackup() {
$.get('/Files/MakeBackup', function (data) {
window.location.href = data;

View File

@@ -336,6 +336,16 @@ function isValidMoney(input) {
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
return (euRegex.test(input) || usRegex.test(input));
}
function initExtraFieldDatePicker(fieldName) {
let inputField = $(`#${fieldName}`);
if (inputField.length > 0) {
inputField.datepicker({
format: getShortDatePattern().pattern,
autoclose: true,
weekStart: getGlobalConfig().firstDayOfWeek
});
}
}
function initDatePicker(input, futureOnly) {
if (futureOnly) {
input.datepicker({
@@ -702,7 +712,7 @@ function getAndValidateExtraFields() {
var extraFieldsVisible = $(".modal.fade.show").find(".extra-field");
extraFieldsVisible.map((index, elem) => {
var extraFieldName = $(elem).children("label").text();
var extraFieldInput = $(elem).children("input");
var extraFieldInput = $(elem).find("input");
var extraFieldValue = extraFieldInput.val();
var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required');
if (extraFieldIsRequired && extraFieldValue.trim() == '') {
@@ -1541,4 +1551,29 @@ function callBackOnEnter(event, callBack) {
if (event.keyCode == 13) {
callBack();
}
}
function populateLocationField(fieldName) {
let populateLocationFieldCallBack = (position) => {
$(`#${fieldName}`).val(`${position.coords.latitude},${position.coords.longitude}`)
};
let populateLocationFieldErrorCallBack = (errMsg) => {
if (errMsg && errMsg.code) {
switch (errMsg.code) {
case 1:
errorToast(errMsg.message);
break;
case 2:
errorToast("Location Unavailable");
break;
}
}
};
if (navigator.geolocation) {
try {
navigator.geolocation.getCurrentPosition(populateLocationFieldCallBack, populateLocationFieldErrorCallBack, { maximumAge: 1000, timeout: 4000, enableHighAccuracy: true });
} catch (err) {
errorToast('Location Services not Enabled');
}
}
}

View File

@@ -336,7 +336,7 @@ function moveRecord(recordId, source, dest) {
}
});
}
function showRecurringReminderSelector(descriptionFieldName) {
function showRecurringReminderSelector(descriptionFieldName, noteFieldName) {
$.get(`/Vehicle/GetRecurringReminderRecordsByVehicleId?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
if (data) {
//prompt user to select a recurring reminder
@@ -356,9 +356,16 @@ function showRecurringReminderSelector(descriptionFieldName) {
}).then(function (result) {
if (result.isConfirmed) {
recurringReminderRecordId = result.value.selectedRecurringReminderData.ids;
var descriptionField = $(`#${descriptionFieldName}`);
let descriptionField = $(`#${descriptionFieldName}`);
let noteField = $(`#${noteFieldName}`);
if (descriptionField.length > 0) {
descriptionField.val(result.value.selectedRecurringReminderData.text);
let descriptionFieldText = result.value.selectedRecurringReminderData.text.join(', ');
descriptionField.val(descriptionFieldText);
}
if (noteField.length > 0 && result.value.selectedRecurringReminderData.text.length > 1) {
result.value.selectedRecurringReminderData.text.map(x => {
noteField.append(`- ${x}\r\n`);
});
}
}
});
@@ -583,7 +590,7 @@ function getAndValidateSelectedRecurringReminder() {
$("#recurringMultipleReminders :checked").map(function () {
selectedRecurringRemindersArray.push({
value: this.value,
text: $(this).parent().find('.form-check-label').text()
text: $(this).attr("data-description")
});
});
if (selectedRecurringRemindersArray.length == 0) {
@@ -596,13 +603,13 @@ function getAndValidateSelectedRecurringReminder() {
return {
hasError: false,
ids: selectedRecurringRemindersArray.map(x=>x.value),
text: selectedRecurringRemindersArray.map(x=>x.text).join(', ')
text: selectedRecurringRemindersArray.map(x=>x.text)
}
}
} else {
//validate single reminder
var selectedRecurringReminder = $("#recurringReminderInput").val();
var selectedRecurringReminderText = $("#recurringReminderInput option:selected").text();
var selectedRecurringReminderText = $("#recurringReminderInput option:selected").attr("data-description");
if (!selectedRecurringReminder || parseInt(selectedRecurringReminder) == 0) {
return {
hasError: true,
@@ -613,42 +620,43 @@ function getAndValidateSelectedRecurringReminder() {
return {
hasError: false,
ids: [selectedRecurringReminder],
text: selectedRecurringReminderText
text: [selectedRecurringReminderText]
}
}
}
}
function getLastOdometerReadingAndIncrement(odometerFieldName) {
Swal.fire({
title: 'Increment Last Reported Odometer Reading',
html: `
$.get(`/Vehicle/GetMaxMileage?vehicleId=${GetVehicleId().vehicleId}`, function (currentOdometer) {
let additionalHtml = isNaN(currentOdometer) || currentOdometer == 0 ? '' : `<span>Current Odometer: ${currentOdometer}</span><br/>`;
Swal.fire({
title: 'Increment Last Reported Odometer Reading',
html: `${additionalHtml}
<input type="text" inputmode="decimal" id="inputOdometerIncrement" class="swal2-input" placeholder="Increment" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const odometerIncrement = parseInt(globalParseFloat($("#inputOdometerIncrement").val()));
if (isNaN(odometerIncrement) || odometerIncrement <= 0) {
Swal.showValidationMessage(`Please enter a non-zero amount to increment`);
}
return { odometerIncrement }
},
}).then(function (result) {
if (result.isConfirmed) {
var amountToIncrement = result.value.odometerIncrement;
$.get(`/Vehicle/GetMaxMileage?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
var newAmount = data + amountToIncrement;
if (!isNaN(newAmount)) {
var odometerField = $(`#${odometerFieldName}`);
if (odometerField.length > 0) {
odometerField.val(newAmount);
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const odometerIncrement = parseInt(globalParseFloat($("#inputOdometerIncrement").val()));
if (isNaN(odometerIncrement) || odometerIncrement < 0) {
Swal.showValidationMessage(`Please enter a positive amount to increment or 0 to use current odometer`);
}
return { odometerIncrement }
},
}).then(function (result) {
if (result.isConfirmed) {
var amountToIncrement = result.value.odometerIncrement;
var newAmount = currentOdometer + amountToIncrement;
if (!isNaN(newAmount)) {
var odometerField = $(`#${odometerFieldName}`);
if (odometerField.length > 0) {
odometerField.val(newAmount);
} else {
errorToast(genericErrorMessage());
}
} else {
errorToast(genericErrorMessage());
}
} else {
errorToast(genericErrorMessage());
}
});
}
}
});
});
}