Merge branch 'Hargata/oidc.userinfo' into Hargata/877
This commit is contained in:
@@ -11,10 +11,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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="LiteDB" Version="5.0.17" />
|
||||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,15 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var planRecord = new PlanRecord()
|
var planRecord = new PlanRecord()
|
||||||
@@ -346,6 +355,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -429,6 +446,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var serviceRecord = new ServiceRecord()
|
var serviceRecord = new ServiceRecord()
|
||||||
@@ -509,6 +534,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -591,6 +624,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var repairRecord = new CollisionRecord()
|
var repairRecord = new CollisionRecord()
|
||||||
@@ -672,6 +713,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -755,6 +804,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var upgradeRecord = new UpgradeRecord()
|
var upgradeRecord = new UpgradeRecord()
|
||||||
@@ -835,6 +892,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -951,6 +1016,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var taxRecord = new TaxRecord()
|
var taxRecord = new TaxRecord()
|
||||||
@@ -1014,6 +1087,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -1113,6 +1194,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var odometerRecord = new OdometerRecord()
|
var odometerRecord = new OdometerRecord()
|
||||||
@@ -1174,6 +1263,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
@@ -1273,6 +1370,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
var gasRecord = new GasRecord()
|
var gasRecord = new GasRecord()
|
||||||
@@ -1352,6 +1457,14 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.StatusCode = 400;
|
Response.StatusCode = 400;
|
||||||
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
|
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
|
try
|
||||||
{
|
{
|
||||||
//retrieve existing record
|
//retrieve existing record
|
||||||
|
|||||||
@@ -130,13 +130,39 @@ namespace CarCareTracker.Controllers
|
|||||||
Content = new FormUrlEncodedContent(httpParams)
|
Content = new FormUrlEncodedContent(httpParams)
|
||||||
};
|
};
|
||||||
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
|
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))
|
if (!string.IsNullOrWhiteSpace(userJwt))
|
||||||
{
|
{
|
||||||
//validate JWT token
|
//validate JWT token
|
||||||
var tokenParser = new JwtSecurityTokenHandler();
|
var tokenParser = new JwtSecurityTokenHandler();
|
||||||
var parsedToken = tokenParser.ReadJwtToken(userJwt);
|
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))
|
if (!string.IsNullOrWhiteSpace(userEmailAddress))
|
||||||
{
|
{
|
||||||
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
|
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
|
||||||
@@ -180,6 +206,126 @@ namespace CarCareTracker.Controllers
|
|||||||
}
|
}
|
||||||
return new RedirectResult("/Login");
|
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]
|
[HttpPost]
|
||||||
public IActionResult Login(LoginModel credentials)
|
public IActionResult Login(LoginModel credentials)
|
||||||
{
|
{
|
||||||
|
|||||||
12
Enum/ExtraFieldType.cs
Normal file
12
Enum/ExtraFieldType.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public enum ExtraFieldType
|
||||||
|
{
|
||||||
|
Text = 0,
|
||||||
|
Number = 1,
|
||||||
|
Decimal = 2,
|
||||||
|
Date = 3,
|
||||||
|
Time = 4,
|
||||||
|
Location = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,6 +238,7 @@ namespace CarCareTracker.Helper
|
|||||||
UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"),
|
UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"),
|
||||||
HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))),
|
HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))),
|
||||||
EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))),
|
EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))),
|
||||||
|
ShowCalendar = CheckBool(CheckString(nameof(UserConfig.ShowCalendar))),
|
||||||
EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))),
|
EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))),
|
||||||
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>() ?? new UserConfig().VisibleTabs,
|
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>() ?? new UserConfig().VisibleTabs,
|
||||||
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>() ?? new UserConfig().TabOrder,
|
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>() ?? new UserConfig().TabOrder,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class StaticHelper
|
public static class StaticHelper
|
||||||
{
|
{
|
||||||
public const string VersionNumber = "1.4.6";
|
public const string VersionNumber = "1.4.7";
|
||||||
public const string DbName = "data/cartracker.db";
|
public const string DbName = "data/cartracker.db";
|
||||||
public const string UserConfigPath = "data/config/userConfig.json";
|
public const string UserConfigPath = "data/config/userConfig.json";
|
||||||
public const string LegacyUserConfigPath = "config/userConfig.json";
|
public const string LegacyUserConfigPath = "config/userConfig.json";
|
||||||
@@ -262,7 +262,9 @@ namespace CarCareTracker.Helper
|
|||||||
//update isrequired setting
|
//update isrequired setting
|
||||||
foreach (ExtraField extraField in recordExtraFields)
|
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
|
//append extra fields
|
||||||
foreach (ExtraField extraField in templateExtraFields)
|
foreach (ExtraField extraField in templateExtraFields)
|
||||||
|
|||||||
@@ -8,13 +8,14 @@
|
|||||||
public string AuthURL { get; set; }
|
public string AuthURL { get; set; }
|
||||||
public string TokenURL { get; set; }
|
public string TokenURL { get; set; }
|
||||||
public string RedirectURL { 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 State { get; set; }
|
||||||
public string CodeChallenge { get; set; }
|
public string CodeChallenge { get; set; }
|
||||||
public bool ValidateState { get; set; } = false;
|
public bool ValidateState { get; set; } = false;
|
||||||
public bool DisableRegularLogin { get; set; } = false;
|
public bool DisableRegularLogin { get; set; } = false;
|
||||||
public bool UsePKCE { get; set; } = false;
|
public bool UsePKCE { get; set; } = false;
|
||||||
public string LogOutURL { get; set; } = "";
|
public string LogOutURL { get; set; } = "";
|
||||||
|
public string UserInfoURL { get; set; } = "";
|
||||||
public string RemoteAuthURL { get {
|
public string RemoteAuthURL { get {
|
||||||
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
|
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
|
||||||
if (UsePKCE)
|
if (UsePKCE)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
public class OpenIDResult
|
public class OpenIDResult
|
||||||
{
|
{
|
||||||
public string id_token { get; set; }
|
public string id_token { get; set; }
|
||||||
|
public string access_token { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Models/OIDC/OpenIDUserInfo.cs
Normal file
7
Models/OIDC/OpenIDUserInfo.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public class OpenIDUserInfo
|
||||||
|
{
|
||||||
|
public string email { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
public bool IsRequired { get; set; }
|
public bool IsRequired { get; set; }
|
||||||
|
public ExtraFieldType FieldType { get; set; } = ExtraFieldType.Text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
public string PreferredGasUnit { get; set; } = string.Empty;
|
public string PreferredGasUnit { get; set; } = string.Empty;
|
||||||
public string PreferredGasMileageUnit { get; set; } = string.Empty;
|
public string PreferredGasMileageUnit { get; set; } = string.Empty;
|
||||||
public bool UseUnitForFuelCost { get; set; }
|
public bool UseUnitForFuelCost { get; set; }
|
||||||
|
public bool ShowCalendar { get; set; }
|
||||||
public List<UserColumnPreference> UserColumnPreferences { get; set; } = new List<UserColumnPreference>();
|
public List<UserColumnPreference> UserColumnPreferences { get; set; } = new List<UserColumnPreference>();
|
||||||
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
|
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
|
||||||
public string UserNameHash { get; set; }
|
public string UserNameHash { get; set; }
|
||||||
|
|||||||
@@ -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>
|
<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>
|
||||||
}
|
}
|
||||||
<li class="nav-item" role="presentation">
|
@if(userConfig.ShowCalendar){
|
||||||
<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 class="nav-item" role="presentation">
|
||||||
</li>
|
<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">
|
<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>
|
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</span></button>
|
||||||
</li>
|
</li>
|
||||||
@@ -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>
|
<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>
|
||||||
}
|
}
|
||||||
<li class="nav-item" role="presentation">
|
@if (userConfig.ShowCalendar){
|
||||||
<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 class="nav-item" role="presentation">
|
||||||
</li>
|
<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">
|
<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>
|
<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>
|
</li>
|
||||||
|
|||||||
@@ -36,8 +36,9 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead class="sticky-top">
|
<thead class="sticky-top">
|
||||||
<tr class="d-flex">
|
<tr class="d-flex">
|
||||||
<th scope="col" class="col-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-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>
|
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -47,11 +48,21 @@
|
|||||||
@foreach (ExtraField extraField in Model.ExtraFields)
|
@foreach (ExtraField extraField in Model.ExtraFields)
|
||||||
{
|
{
|
||||||
<script>
|
<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>
|
</script>
|
||||||
<tr class="d-flex">
|
<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-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>
|
<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>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -99,6 +110,12 @@
|
|||||||
extraFieldToEdit.isRequired = $(checkbox).is(":checked");
|
extraFieldToEdit.isRequired = $(checkbox).is(":checked");
|
||||||
updateExtraFields();
|
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) {
|
function deleteExtraField(fieldId) {
|
||||||
extraFields = extraFields.filter(x => x.name != fieldId);
|
extraFields = extraFields.filter(x => x.name != fieldId);
|
||||||
updateExtraFields();
|
updateExtraFields();
|
||||||
|
|||||||
@@ -81,9 +81,9 @@
|
|||||||
</div>
|
</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%;">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,6 +165,14 @@
|
|||||||
<div class="col-md-6 col-12">
|
<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">
|
<input type="text" readonly id="inputOIDCToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.TokenURL">
|
||||||
</div>
|
</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>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-6 col-12">
|
<div class="col-md-6 col-12">
|
||||||
|
|||||||
@@ -86,6 +86,10 @@
|
|||||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies">
|
<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>
|
<label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||||
{
|
{
|
||||||
|
|||||||
17
Views/Login/RemoteAuthDebug.cshtml
Normal file
17
Views/Login/RemoteAuthDebug.cshtml
Normal 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>
|
||||||
|
|
||||||
@@ -52,14 +52,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
42
Views/Vehicle/_ExtraField.cshtml
Normal file
42
Views/Vehicle/_ExtraField.cshtml
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Views/Vehicle/_ExtraFieldMultiple.cshtml
Normal file
49
Views/Vehicle/_ExtraFieldMultiple.cshtml
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
{
|
{
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="d-flex justify-content-between">
|
<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">
|
<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-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>
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||||
|
|||||||
@@ -97,14 +97,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.GasRecord.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.GasRecord.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -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)")">
|
<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>
|
<label for="gasRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||||
<select multiple class="form-select" id="gasRecordTag"></select>
|
<select multiple class="form-select" id="gasRecordTag"></select>
|
||||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
@await Html.PartialAsync("_ExtraFieldMultiple", 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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -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)")">
|
<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>
|
<label for="genericRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||||
<select multiple class="form-select" id="genericRecordTag"></select>
|
<select multiple class="form-select" id="genericRecordTag"></select>
|
||||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
@await Html.PartialAsync("_ExtraFieldMultiple", 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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -72,14 +72,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -26,14 +26,7 @@
|
|||||||
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
<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>
|
<label for="odometerRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||||
<select multiple class="form-select" id="odometerRecordTag"></select>
|
<select multiple class="form-select" id="odometerRecordTag"></select>
|
||||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
@await Html.PartialAsync("_ExtraFieldMultiple", 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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -40,14 +40,7 @@
|
|||||||
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
|
<!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>
|
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (!isNew)
|
@if (!isNew)
|
||||||
{
|
{
|
||||||
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>
|
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>
|
||||||
|
|||||||
@@ -40,14 +40,7 @@
|
|||||||
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
|
<!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>
|
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row swimlane">
|
<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="row">
|
||||||
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
||||||
<span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
|
<span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
@await Html.PartialAsync("_PlanRecordItem", planRecord)
|
@await Html.PartialAsync("_PlanRecordItem", planRecord)
|
||||||
}
|
}
|
||||||
</div>
|
</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="row">
|
||||||
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
||||||
<span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
|
<span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
@await Html.PartialAsync("_PlanRecordItem", planRecord)
|
@await Html.PartialAsync("_PlanRecordItem", planRecord)
|
||||||
}
|
}
|
||||||
</div>
|
</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="row">
|
||||||
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
|
||||||
<span class="display-7">@translator.Translate(userLanguage,"Done")</span>
|
<span class="display-7">@translator.Translate(userLanguage,"Done")</span>
|
||||||
|
|||||||
@@ -85,6 +85,20 @@
|
|||||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</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>
|
<th scope="col" data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@translator.Translate(userLanguage, "Notes")</th>
|
||||||
@if (hasRefresh)
|
@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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<td data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@StaticHelper.TruncateStrings(reminderRecord.Notes)</td>
|
||||||
@if (hasRefresh)
|
@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)
|
@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>
|
<button type="button" class="btn btn-secondary" onclick="markDoneReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-check-lg"></i></button>
|
||||||
}
|
}
|
||||||
</td>
|
</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>
|
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -52,14 +52,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -50,14 +50,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -41,14 +41,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -52,14 +52,7 @@
|
|||||||
<!option value="@tag">@tag</!option>
|
<!option value="@tag">@tag</!option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", Model.ExtraFields)
|
||||||
{
|
|
||||||
var elementId = Guid.NewGuid();
|
|
||||||
<div class="extra-field">
|
|
||||||
<label for="@elementId">@field.Name</label>
|
|
||||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{
|
{
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="d-flex justify-content-between">
|
<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">
|
<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-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>
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||||
|
|||||||
@@ -36,14 +36,7 @@
|
|||||||
<input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model">
|
<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>
|
<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">
|
<input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
|
||||||
@foreach (ExtraField field in Model.ExtraFields)
|
@await Html.PartialAsync("_ExtraField", 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>
|
|
||||||
}
|
|
||||||
<label for="inputIdentifier" class="@(!Model.ExtraFields.Any() ? "d-none" : "")">@translator.Translate(userLanguage, "Identifier")</label>
|
<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" )>
|
<select class="form-select @(!Model.ExtraFields.Any() ? "d-none" : "")" id="inputIdentifier" )>
|
||||||
<!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option>
|
<!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"EnableAutoReminderRefresh": false,
|
"EnableAutoReminderRefresh": false,
|
||||||
"EnableAutoOdometerInsert": false,
|
"EnableAutoOdometerInsert": false,
|
||||||
"EnableShopSupplies": false,
|
"EnableShopSupplies": false,
|
||||||
|
"ShowCalendar": true,
|
||||||
"EnableExtraFieldColumns": false,
|
"EnableExtraFieldColumns": false,
|
||||||
"UseUKMPG": false,
|
"UseUKMPG": false,
|
||||||
"UseThreeDecimalGasCost": true,
|
"UseThreeDecimalGasCost": true,
|
||||||
|
|||||||
@@ -150,6 +150,11 @@
|
|||||||
<input type="text" id="inputOIDCTokenURL" class="form-control">
|
<input type="text" id="inputOIDCTokenURL" class="form-control">
|
||||||
<small class="text-body-secondary">Token URL from Provider</small>
|
<small class="text-body-secondary">Token URL from Provider</small>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label for="inputOIDCRedirectURL">LubeLogger URL</label>
|
<label for="inputOIDCRedirectURL">LubeLogger URL</label>
|
||||||
<input type="text" id="inputOIDCRedirectURL" class="form-control">
|
<input type="text" id="inputOIDCRedirectURL" class="form-control">
|
||||||
@@ -332,6 +337,7 @@ function generateConfig(){
|
|||||||
ClientSecret: $("#inputOIDCClientSecret").val(),
|
ClientSecret: $("#inputOIDCClientSecret").val(),
|
||||||
AuthURL: $("#inputOIDCAuthURL").val(),
|
AuthURL: $("#inputOIDCAuthURL").val(),
|
||||||
TokenURL: $("#inputOIDCTokenURL").val(),
|
TokenURL: $("#inputOIDCTokenURL").val(),
|
||||||
|
UserInfoURL: $("#inputOIDCUserInfoURL").val(),
|
||||||
RedirectURL: redirectUrl,
|
RedirectURL: redirectUrl,
|
||||||
Scope: $("#inputOIDCScope").val(),
|
Scope: $("#inputOIDCScope").val(),
|
||||||
ValidateState: $("#inputOIDCValidateState").is(":checked"),
|
ValidateState: $("#inputOIDCValidateState").is(":checked"),
|
||||||
@@ -405,6 +411,7 @@ function generateConfig(){
|
|||||||
dockerConfig.push(`OpenIDConfig__ClientSecret="${$('#inputOIDCClientSecret').val()}"`);
|
dockerConfig.push(`OpenIDConfig__ClientSecret="${$('#inputOIDCClientSecret').val()}"`);
|
||||||
dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`);
|
dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`);
|
||||||
dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`);
|
dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`);
|
||||||
|
dockerConfig.push(`OpenIDConfig__UserInfoURL="${$('#inputOIDCUserInfoURL').val()}"`);
|
||||||
dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`);
|
dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`);
|
||||||
dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`);
|
dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`);
|
||||||
dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`);
|
dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`);
|
||||||
|
|||||||
@@ -48,12 +48,10 @@ html {
|
|||||||
.swimlane{
|
.swimlane{
|
||||||
height:100%;
|
height:100%;
|
||||||
}
|
}
|
||||||
.swimlane.mid {
|
|
||||||
|
.swimlane:not(:last-child) {
|
||||||
border-right-style: solid;
|
border-right-style: solid;
|
||||||
}
|
}
|
||||||
.swimlane.end {
|
|
||||||
border-left-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showOnPrint {
|
.showOnPrint {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -71,6 +71,7 @@ function updateSettings() {
|
|||||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||||
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
||||||
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
|
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
|
||||||
|
showCalendar: $("#showCalendar").is(":checked"),
|
||||||
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
|
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
|
||||||
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
|
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
|
||||||
preferredGasUnit: $("#preferredGasUnit").val(),
|
preferredGasUnit: $("#preferredGasUnit").val(),
|
||||||
|
|||||||
@@ -336,6 +336,16 @@ function isValidMoney(input) {
|
|||||||
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
|
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
|
||||||
return (euRegex.test(input) || usRegex.test(input));
|
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) {
|
function initDatePicker(input, futureOnly) {
|
||||||
if (futureOnly) {
|
if (futureOnly) {
|
||||||
input.datepicker({
|
input.datepicker({
|
||||||
@@ -702,7 +712,7 @@ function getAndValidateExtraFields() {
|
|||||||
var extraFieldsVisible = $(".modal.fade.show").find(".extra-field");
|
var extraFieldsVisible = $(".modal.fade.show").find(".extra-field");
|
||||||
extraFieldsVisible.map((index, elem) => {
|
extraFieldsVisible.map((index, elem) => {
|
||||||
var extraFieldName = $(elem).children("label").text();
|
var extraFieldName = $(elem).children("label").text();
|
||||||
var extraFieldInput = $(elem).children("input");
|
var extraFieldInput = $(elem).find("input");
|
||||||
var extraFieldValue = extraFieldInput.val();
|
var extraFieldValue = extraFieldInput.val();
|
||||||
var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required');
|
var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required');
|
||||||
if (extraFieldIsRequired && extraFieldValue.trim() == '') {
|
if (extraFieldIsRequired && extraFieldValue.trim() == '') {
|
||||||
@@ -1542,3 +1552,28 @@ function callBackOnEnter(event, callBack) {
|
|||||||
callBack();
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user