Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
085eb2a9a0 | ||
|
|
f20c04d523 | ||
|
|
b7b7d6ad3e | ||
|
|
299444d767 | ||
|
|
0dbaa68fc0 | ||
|
|
80fd9e136a | ||
|
|
ba27db5f75 | ||
|
|
4629271fb2 | ||
|
|
91177af98b | ||
|
|
26281d1cbb | ||
|
|
a80c6e12ad | ||
|
|
8e208e8791 | ||
|
|
6e87c2d5cc | ||
|
|
3d96984735 | ||
|
|
4626fdf04a | ||
|
|
a2c013ec43 | ||
|
|
9262131936 | ||
|
|
02c5302653 | ||
|
|
fa73b61e3f | ||
|
|
96ade10289 | ||
|
|
acf16a6c28 | ||
|
|
dedf8fc2f7 | ||
|
|
7099652083 | ||
|
|
d0f9795e63 | ||
|
|
0ffc469ac8 | ||
|
|
b5ea3c63a1 | ||
|
|
5901531ddf | ||
|
|
814aa8d22b | ||
|
|
d3aa55f987 | ||
|
|
8b7fb7ef0f | ||
|
|
0fab57d19c | ||
|
|
0d8822c496 | ||
|
|
7235931f38 | ||
|
|
410f45c6c2 | ||
|
|
9a44d80be4 | ||
|
|
2f52ed64ba | ||
|
|
dd68dec05c | ||
|
|
a8bc9f90e1 | ||
|
|
8759216dd6 | ||
|
|
0851e0a222 | ||
|
|
40f1f1380e | ||
|
|
2a5ff7a911 | ||
|
|
0590f991d2 | ||
|
|
c6a96df3e9 | ||
|
|
0088c74b20 | ||
|
|
6af0d8b88e | ||
|
|
5204a71b00 | ||
|
|
8b2866b89b | ||
|
|
65f638f336 | ||
|
|
f92e95e01c | ||
|
|
2aeae70060 | ||
|
|
98b1cd9bc5 | ||
|
|
a02684f921 | ||
|
|
13ddc6309e | ||
|
|
2b2fd3ae28 | ||
|
|
2dd3dc1718 | ||
|
|
fe1b96d58c | ||
|
|
8dfee0fd12 | ||
|
|
275b60aa14 | ||
|
|
dd14b71e68 | ||
|
|
849171c4df | ||
|
|
2183c77606 | ||
|
|
bedf7bad34 | ||
|
|
802c7923d5 | ||
|
|
95c8cd19f8 | ||
|
|
e700a5f1c2 | ||
|
|
7fd7f7e29d | ||
|
|
902ddd0269 | ||
|
|
0a6424f8c0 | ||
|
|
3a36c624ba | ||
|
|
1329beb808 | ||
|
|
0e8ef0180f | ||
|
|
660f089035 | ||
|
|
04f90ae6a8 | ||
|
|
30b4a73fef | ||
|
|
b7158f3bf0 | ||
|
|
f46bbe9963 | ||
|
|
9f73068a9e | ||
|
|
779b18802b | ||
|
|
dce9acf47f | ||
|
|
c9a925f548 | ||
|
|
4eeec7887a | ||
|
|
156c781bd7 | ||
|
|
296cf92022 | ||
|
|
d5f0e57c3b | ||
|
|
86ba6200fc | ||
|
|
ac4ea07319 | ||
|
|
6d380de603 | ||
|
|
e789bc6925 | ||
|
|
f7481be91c | ||
|
|
a8151bcf0e | ||
|
|
0b6a6787bb | ||
|
|
7302982366 | ||
|
|
19b7dfc90a | ||
|
|
29825a6ad6 | ||
|
|
f6493f0496 | ||
|
|
c2d61eecc0 | ||
|
|
2a4354c52e | ||
|
|
e7dc4f67f5 | ||
|
|
8584d2cf9c |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Checklist**
|
||||
Please make sure you have performed the following steps before opening a new bug ticket, change `[ ]` to `[x]` to mark it as done
|
||||
|
||||
- [ ] I have read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
|
||||
- [ ] I have searched through existing issues.
|
||||
|
||||
**Description**
|
||||
<!-- Describe the bug below this line -->
|
||||
|
||||
**Platform**
|
||||
- [ ] Docker Image
|
||||
- [ ] Windows Standalone Executable
|
||||
|
||||
**Browser Console Errors(F12)**
|
||||
<!-- Attach a screenshot or codeblock containing the browser console error -->
|
||||
|
||||
**App/Container Console Error**
|
||||
<!-- Attach a screenshot or codeblock containing the app/container console error -->
|
||||
|
||||
**Screenshots(optional)**
|
||||
<!-- Attach a screenshot describing the bug -->
|
||||
@@ -13,7 +13,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.1" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,8 +3,6 @@ using CarCareTracker.Logic;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Diagnostics;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
using System.Drawing;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CarCareTracker.Helper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
|
||||
@@ -72,12 +72,18 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var maxMileage = vehicleReminders.Max(x => x.Mileage) + 1000;
|
||||
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, maxMileage, DateTime.Now);
|
||||
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
|
||||
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate} - {x.Description}" }).ToList();
|
||||
reminders.AddRange(reminderUrgency);
|
||||
}
|
||||
}
|
||||
return PartialView("_Calendar", reminders);
|
||||
}
|
||||
public IActionResult ViewCalendarReminder(int reminderId)
|
||||
{
|
||||
var reminder = _reminderRecordDataAccess.GetReminderRecordById(reminderId);
|
||||
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List<ReminderRecord> { reminder }, reminder.Mileage + 1000, DateTime.Now).FirstOrDefault();
|
||||
return PartialView("_ReminderRecordCalendarModal", reminderUrgency);
|
||||
}
|
||||
public IActionResult Settings()
|
||||
{
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
@@ -99,6 +105,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
return View();
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
public IActionResult GetExtraFieldsModal(int importMode = 0)
|
||||
{
|
||||
var recordExtraFields = _extraFieldDataAccess.GetExtraFieldsById(importMode);
|
||||
@@ -108,6 +115,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return PartialView("_ExtraFields", recordExtraFields);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
public IActionResult UpdateExtraFields(RecordExtraField record)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -4,9 +4,7 @@ using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
@@ -15,16 +13,19 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
private IDataProtector _dataProtector;
|
||||
private ILoginLogic _loginLogic;
|
||||
private IConfigHelper _config;
|
||||
private readonly ILogger<LoginController> _logger;
|
||||
public LoginController(
|
||||
ILogger<LoginController> logger,
|
||||
IDataProtectionProvider securityProvider,
|
||||
ILoginLogic loginLogic
|
||||
)
|
||||
ILoginLogic loginLogic,
|
||||
IConfigHelper config
|
||||
)
|
||||
{
|
||||
_dataProtector = securityProvider.CreateProtector("login");
|
||||
_logger = logger;
|
||||
_loginLogic = loginLogic;
|
||||
_config = config;
|
||||
}
|
||||
public IActionResult Index(string redirectURL = "")
|
||||
{
|
||||
@@ -42,6 +43,91 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public IActionResult GetRemoteLoginLink()
|
||||
{
|
||||
var remoteAuthConfig = _config.GetOpenIDConfig();
|
||||
var generatedState = Guid.NewGuid().ToString().Substring(0, 8);
|
||||
remoteAuthConfig.State = generatedState;
|
||||
if (remoteAuthConfig.ValidateState)
|
||||
{
|
||||
Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) });
|
||||
}
|
||||
var remoteAuthURL = remoteAuthConfig.RemoteAuthURL;
|
||||
return Json(remoteAuthURL);
|
||||
}
|
||||
public async Task<IActionResult> RemoteAuth(string code, string state = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
//received code from OIDC provider
|
||||
//create http client to retrieve user token from OIDC
|
||||
var httpClient = new HttpClient();
|
||||
var openIdConfig = _config.GetOpenIDConfig();
|
||||
//check if validate state is enabled.
|
||||
if (openIdConfig.ValidateState)
|
||||
{
|
||||
var storedStateValue = Request.Cookies["OIDC_STATE"];
|
||||
if (!string.IsNullOrWhiteSpace(storedStateValue))
|
||||
{
|
||||
Response.Cookies.Delete("OIDC_STATE");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state)
|
||||
{
|
||||
_logger.LogInformation("Failed OIDC State Validation - Try disabling state validation if you are confident this is not a malicious attempt.");
|
||||
return new RedirectResult("/Login");
|
||||
}
|
||||
}
|
||||
var httpParams = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("client_id", openIdConfig.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
|
||||
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
|
||||
};
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
|
||||
{
|
||||
Content = new FormUrlEncodedContent(httpParams)
|
||||
};
|
||||
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
|
||||
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(userJwt))
|
||||
{
|
||||
//validate JWT token
|
||||
var tokenParser = new JwtSecurityTokenHandler();
|
||||
var parsedToken = tokenParser.ReadJwtToken(userJwt);
|
||||
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
|
||||
if (!string.IsNullOrWhiteSpace(userEmailAddress))
|
||||
{
|
||||
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
|
||||
if (userData.Id != default)
|
||||
{
|
||||
AuthCookie authCookie = new AuthCookie
|
||||
{
|
||||
UserData = userData,
|
||||
ExpiresOn = DateTime.Now.AddDays(1)
|
||||
};
|
||||
var serializedCookie = JsonSerializer.Serialize(authCookie);
|
||||
var encryptedCookie = _dataProtector.Protect(serializedCookie);
|
||||
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
|
||||
return new RedirectResult("/Home");
|
||||
} else
|
||||
{
|
||||
_logger.LogInformation($"User {userEmailAddress} tried to login via OpenID but is not a registered user in LubeLogger.");
|
||||
return View("OpenIDRegistration", model: userEmailAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return new RedirectResult("/Login");
|
||||
}
|
||||
return new RedirectResult("/Login");
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult Login(LoginModel credentials)
|
||||
{
|
||||
@@ -81,6 +167,27 @@ namespace CarCareTracker.Controllers
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult RegisterOpenIdUser(LoginModel credentials)
|
||||
{
|
||||
var result = _loginLogic.RegisterOpenIdUser(credentials);
|
||||
if (result.Success)
|
||||
{
|
||||
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = credentials.EmailAddress });
|
||||
if (userData.Id != default)
|
||||
{
|
||||
AuthCookie authCookie = new AuthCookie
|
||||
{
|
||||
UserData = userData,
|
||||
ExpiresOn = DateTime.Now.AddDays(1)
|
||||
};
|
||||
var serializedCookie = JsonSerializer.Serialize(authCookie);
|
||||
var encryptedCookie = _dataProtector.Protect(serializedCookie);
|
||||
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult RequestResetPassword(LoginModel credentials)
|
||||
{
|
||||
var result = _loginLogic.RequestResetPassword(credentials);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using CarCareTracker.External.Implementations;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Npgsql;
|
||||
using System.Data.Common;
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
|
||||
@@ -154,6 +154,37 @@ namespace CarCareTracker.Controllers
|
||||
_dataAccess.DeleteVehicle(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DuplicateVehicleCollaborators(int sourceVehicleId, int destVehicleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
//retrieve collaborators for both source and destination vehicle id.
|
||||
if (_userLogic.UserCanEditVehicle(GetUserID(), sourceVehicleId) && _userLogic.UserCanEditVehicle(GetUserID(), destVehicleId))
|
||||
{
|
||||
var sourceCollaborators = _userLogic.GetCollaboratorsForVehicle(sourceVehicleId).Select(x => x.UserVehicle.UserId).ToList();
|
||||
var destCollaborators = _userLogic.GetCollaboratorsForVehicle(destVehicleId).Select(x => x.UserVehicle.UserId).ToList();
|
||||
sourceCollaborators.RemoveAll(x => destCollaborators.Contains(x));
|
||||
if (sourceCollaborators.Any())
|
||||
{
|
||||
foreach (int collaboratorId in sourceCollaborators)
|
||||
{
|
||||
_userLogic.AddUserAccessToVehicle(collaboratorId, destVehicleId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "Both vehicles already have identical collaborators" });
|
||||
}
|
||||
}
|
||||
return Json(new OperationResponse { Success = true, Message = "Collaborators Copied" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
}
|
||||
#region "Bulk Imports and Exports"
|
||||
[HttpGet]
|
||||
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
|
||||
@@ -426,6 +457,16 @@ namespace CarCareTracker.Controllers
|
||||
if (convertedRecord.Gallons > 0)
|
||||
{
|
||||
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(new OdometerRecord
|
||||
{
|
||||
Date = convertedRecord.Date,
|
||||
VehicleId = convertedRecord.VehicleId,
|
||||
Mileage = convertedRecord.Mileage,
|
||||
Notes = $"Auto Insert From Gas Record via CSV Import. {convertedRecord.Notes}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.ServiceRecord)
|
||||
@@ -441,6 +482,16 @@ namespace CarCareTracker.Controllers
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(new OdometerRecord
|
||||
{
|
||||
Date = convertedRecord.Date,
|
||||
VehicleId = convertedRecord.VehicleId,
|
||||
Mileage = convertedRecord.Mileage,
|
||||
Notes = $"Auto Insert From Service Record via CSV Import. {convertedRecord.Notes}"
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.OdometerRecord)
|
||||
{
|
||||
@@ -486,6 +537,16 @@ namespace CarCareTracker.Controllers
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(new OdometerRecord
|
||||
{
|
||||
Date = convertedRecord.Date,
|
||||
VehicleId = convertedRecord.VehicleId,
|
||||
Mileage = convertedRecord.Mileage,
|
||||
Notes = $"Auto Insert From Repair Record via CSV Import. {convertedRecord.Notes}"
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
@@ -500,6 +561,16 @@ namespace CarCareTracker.Controllers
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList()
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(new OdometerRecord
|
||||
{
|
||||
Date = convertedRecord.Date,
|
||||
VehicleId = convertedRecord.VehicleId,
|
||||
Mileage = convertedRecord.Mileage,
|
||||
Notes = $"Auto Insert From Upgrade Record via CSV Import. {convertedRecord.Notes}"
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.SupplyRecord)
|
||||
{
|
||||
@@ -672,11 +743,16 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
//move files from temp.
|
||||
serviceRecord.Files = serviceRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
|
||||
if (result && serviceRecord.Supplies.Any())
|
||||
if (serviceRecord.Supplies.Any())
|
||||
{
|
||||
RequisitionSupplyRecordsByUsage(serviceRecord.Supplies);
|
||||
serviceRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(serviceRecord.Supplies, DateTime.Parse(serviceRecord.Date), serviceRecord.Description);
|
||||
}
|
||||
//push back any reminders
|
||||
if (serviceRecord.ReminderRecordId != default)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(serviceRecord.ReminderRecordId);
|
||||
}
|
||||
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
@@ -700,6 +776,7 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
RequisitionHistory = result.RequisitionHistory,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_ServiceRecordModal", convertedResult);
|
||||
@@ -743,11 +820,16 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
//move files from temp.
|
||||
collisionRecord.Files = collisionRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
|
||||
if (result && collisionRecord.Supplies.Any())
|
||||
if (collisionRecord.Supplies.Any())
|
||||
{
|
||||
RequisitionSupplyRecordsByUsage(collisionRecord.Supplies);
|
||||
collisionRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(collisionRecord.Supplies, DateTime.Parse(collisionRecord.Date), collisionRecord.Description);
|
||||
}
|
||||
//push back any reminders
|
||||
if (collisionRecord.ReminderRecordId != default)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(collisionRecord.ReminderRecordId);
|
||||
}
|
||||
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
@@ -771,6 +853,7 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
RequisitionHistory = result.RequisitionHistory,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.RepairRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_CollisionRecordModal", convertedResult);
|
||||
@@ -805,17 +888,19 @@ namespace CarCareTracker.Controllers
|
||||
var recurringFees = result.Where(x => x.IsRecurring);
|
||||
if (recurringFees.Any())
|
||||
{
|
||||
foreach(TaxRecord recurringFee in recurringFees)
|
||||
foreach (TaxRecord recurringFee in recurringFees)
|
||||
{
|
||||
var newDate = new DateTime();
|
||||
if (recurringFee.RecurringInterval != ReminderMonthInterval.Other)
|
||||
{
|
||||
newDate = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
newDate = recurringFee.Date.AddMonths(recurringFee.CustomMonthInterval);
|
||||
}
|
||||
if (DateTime.Now > newDate){
|
||||
if (DateTime.Now > newDate)
|
||||
{
|
||||
recurringFee.IsRecurring = false;
|
||||
var newRecurringFee = new TaxRecord()
|
||||
{
|
||||
@@ -842,6 +927,11 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
//move files from temp.
|
||||
taxRecord.Files = taxRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
//push back any reminders
|
||||
if (taxRecord.ReminderRecordId != default)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(taxRecord.ReminderRecordId);
|
||||
}
|
||||
var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord());
|
||||
return Json(result);
|
||||
}
|
||||
@@ -890,6 +980,7 @@ namespace CarCareTracker.Controllers
|
||||
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
var viewModel = new ReportViewModel();
|
||||
//get totalCostMakeUp
|
||||
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
|
||||
@@ -907,10 +998,12 @@ namespace CarCareTracker.Controllers
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, 0));
|
||||
viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key.MonthName,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
DistanceTraveled = x.Any(y => y.MinMileage != default) ? x.Max(y => y.MaxMileage) - x.Where(y => y.MinMileage != default).Min(y => y.MinMileage) : 0
|
||||
}).ToList();
|
||||
//get reminders
|
||||
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
@@ -962,7 +1055,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y=>y.Cost)
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
viewModel.FuelMileageForVehicleByMonth = monthlyMileageData;
|
||||
return PartialView("_Report", viewModel);
|
||||
@@ -1033,8 +1126,9 @@ namespace CarCareTracker.Controllers
|
||||
public IActionResult GetVehicleAttachments(int vehicleId, List<ImportMode> exportTabs)
|
||||
{
|
||||
List<GenericReportModel> attachmentData = new List<GenericReportModel>();
|
||||
if (exportTabs.Contains(ImportMode.ServiceRecord)){
|
||||
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x=>x.Files.Any());
|
||||
if (exportTabs.Contains(ImportMode.ServiceRecord))
|
||||
{
|
||||
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
@@ -1101,7 +1195,8 @@ namespace CarCareTracker.Controllers
|
||||
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
return Json(new OperationResponse { Success = true, Message = result });
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "No Attachments Found" });
|
||||
}
|
||||
@@ -1239,10 +1334,16 @@ namespace CarCareTracker.Controllers
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.OdometerRecord))
|
||||
{
|
||||
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, year));
|
||||
}
|
||||
var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key.MonthName,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
DistanceTraveled = x.Any(y => y.MinMileage != default) ? x.Max(y => y.MaxMileage) - x.Where(y => y.MinMileage != default).Min(y => y.MinMileage) : 0
|
||||
}).ToList();
|
||||
return PartialView("_GasCostByMonthReport", groupedRecord);
|
||||
}
|
||||
@@ -1327,15 +1428,48 @@ namespace CarCareTracker.Controllers
|
||||
result = result.OrderByDescending(x => x.Urgency).ToList();
|
||||
return PartialView("_ReminderRecords", result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
result.RemoveAll(x => !x.IsRecurring);
|
||||
return PartialView("_RecurringReminderSelector", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult PushbackRecurringReminderRecord(int reminderRecordId)
|
||||
{
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
|
||||
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder);
|
||||
//save to db.
|
||||
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
|
||||
var result = PushbackRecurringReminderRecordWithChecks(reminderRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
private bool PushbackRecurringReminderRecordWithChecks(int reminderRecordId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
|
||||
if (existingReminder is not null && existingReminder.Id != default && existingReminder.IsRecurring)
|
||||
{
|
||||
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder);
|
||||
//save to db.
|
||||
var reminderUpdateResult = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
|
||||
if (!reminderUpdateResult)
|
||||
{
|
||||
_logger.LogError("Unable to update reminder either because the reminder no longer exists or is no longer recurring");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Unable to update reminder because it no longer exists.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveReminderRecordToVehicleId(ReminderRecordInput reminderRecord)
|
||||
{
|
||||
@@ -1415,11 +1549,16 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
//move files from temp.
|
||||
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
|
||||
if (result && upgradeRecord.Supplies.Any())
|
||||
if (upgradeRecord.Supplies.Any())
|
||||
{
|
||||
RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies);
|
||||
upgradeRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Description);
|
||||
}
|
||||
//push back any reminders
|
||||
if (upgradeRecord.ReminderRecordId != default)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(upgradeRecord.ReminderRecordId);
|
||||
}
|
||||
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
@@ -1443,6 +1582,7 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
RequisitionHistory = result.RequisitionHistory,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_UpgradeRecordModal", convertedResult);
|
||||
@@ -1468,7 +1608,7 @@ namespace CarCareTracker.Controllers
|
||||
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
|
||||
result = result.Where(x=>x.Pinned).ToList();
|
||||
result = result.Where(x => x.Pinned).ToList();
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
@@ -1494,6 +1634,24 @@ namespace CarCareTracker.Controllers
|
||||
var result = _noteDataAccess.DeleteNoteById(noteId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult PinNotes(List<int> noteIds, bool isToggle = false, bool pinStatus = false)
|
||||
{
|
||||
var result = false;
|
||||
foreach(int noteId in noteIds)
|
||||
{
|
||||
var existingNote = _noteDataAccess.GetNoteById(noteId);
|
||||
if (isToggle)
|
||||
{
|
||||
existingNote.Pinned = !existingNote.Pinned;
|
||||
} else
|
||||
{
|
||||
existingNote.Pinned = pinStatus;
|
||||
}
|
||||
result = _noteDataAccess.SaveNoteToVehicle(existingNote);
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Supply Records"
|
||||
private List<string> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
|
||||
@@ -1515,20 +1673,41 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private void RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage)
|
||||
private List<SupplyUsageHistory> RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage, DateTime dateRequisitioned, string usageDescription)
|
||||
{
|
||||
foreach(SupplyUsage supply in supplyUsage)
|
||||
List<SupplyUsageHistory> results = new List<SupplyUsageHistory>();
|
||||
foreach (SupplyUsage supply in supplyUsage)
|
||||
{
|
||||
//get supply record.
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
|
||||
var unitCost = (result.Quantity != 0 ) ? result.Cost / result.Quantity : 0;
|
||||
var unitCost = (result.Quantity != 0) ? result.Cost / result.Quantity : 0;
|
||||
//deduct quantity used.
|
||||
result.Quantity -= supply.Quantity;
|
||||
//deduct cost.
|
||||
result.Cost -= (supply.Quantity * unitCost);
|
||||
//check decimal places to ensure that it always has a max of 3 decimal places.
|
||||
var roundedDecimal = decimal.Round(result.Cost, 3);
|
||||
if (roundedDecimal != result.Cost)
|
||||
{
|
||||
//Too many decimals
|
||||
result.Cost = roundedDecimal;
|
||||
}
|
||||
//create new requisitionrrecord
|
||||
var requisitionRecord = new SupplyUsageHistory
|
||||
{
|
||||
Date = dateRequisitioned,
|
||||
Description = usageDescription,
|
||||
Quantity = supply.Quantity,
|
||||
Cost = (supply.Quantity * unitCost)
|
||||
};
|
||||
result.RequisitionHistory.Add(requisitionRecord);
|
||||
//save
|
||||
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(result);
|
||||
requisitionRecord.Description = result.Description; //change the name of the description for plan/service/repair/upgrade records
|
||||
requisitionRecord.PartNumber = result.PartNumber; //populate part number if not displayed in supplies modal.
|
||||
results.Add(requisitionRecord);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
@@ -1598,6 +1777,7 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
RequisitionHistory = result.RequisitionHistory,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_SupplyRecordModal", convertedResult);
|
||||
@@ -1628,21 +1808,21 @@ namespace CarCareTracker.Controllers
|
||||
planRecord.DateModified = DateTime.Now.ToString("G");
|
||||
//move files from temp.
|
||||
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
|
||||
if (result && planRecord.Supplies.Any())
|
||||
if (planRecord.Supplies.Any())
|
||||
{
|
||||
RequisitionSupplyRecordsByUsage(planRecord.Supplies);
|
||||
planRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description);
|
||||
}
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord)
|
||||
{
|
||||
//check if template name already taken.
|
||||
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x=>x.Description == planRecord.Description).Any();
|
||||
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
|
||||
if (existingRecord)
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle"});
|
||||
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle" });
|
||||
}
|
||||
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
|
||||
@@ -1678,15 +1858,24 @@ namespace CarCareTracker.Controllers
|
||||
return Json(new OperationResponse { Success = false, Message = string.Join("<br>", supplyAvailability) });
|
||||
}
|
||||
}
|
||||
if (existingRecord.ReminderRecordId != default)
|
||||
{
|
||||
//check if reminder still exists and is still recurring.
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(existingRecord.ReminderRecordId);
|
||||
if (existingReminder is null || existingReminder.Id == default || !existingReminder.IsRecurring)
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "Missing or Non-recurring Reminder, Please Delete This Template and Recreate It." });
|
||||
}
|
||||
}
|
||||
//populate createdDate
|
||||
existingRecord.DateCreated = DateTime.Now.ToString("G");
|
||||
existingRecord.DateModified = DateTime.Now.ToString("G");
|
||||
existingRecord.Id = default;
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
|
||||
if (result && existingRecord.Supplies.Any())
|
||||
if (existingRecord.Supplies.Any())
|
||||
{
|
||||
RequisitionSupplyRecordsByUsage(existingRecord.Supplies);
|
||||
existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description);
|
||||
}
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
|
||||
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
[HttpGet]
|
||||
@@ -1695,6 +1884,16 @@ namespace CarCareTracker.Controllers
|
||||
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetAddPlanRecordPartialView(PlanRecordInput? planModel)
|
||||
{
|
||||
if (planModel is not null)
|
||||
{
|
||||
planModel.ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields;
|
||||
return PartialView("_PlanRecordModal", planModel);
|
||||
}
|
||||
return PartialView("_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult UpdatePlanRecordProgress(int planRecordId, PlanProgress planProgress, int odometer = 0)
|
||||
{
|
||||
var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId);
|
||||
@@ -1726,6 +1925,7 @@ namespace CarCareTracker.Controllers
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files,
|
||||
RequisitionHistory = existingRecord.RequisitionHistory,
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
|
||||
@@ -1741,6 +1941,7 @@ namespace CarCareTracker.Controllers
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files,
|
||||
RequisitionHistory = existingRecord.RequisitionHistory,
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
|
||||
@@ -1756,10 +1957,16 @@ namespace CarCareTracker.Controllers
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files,
|
||||
RequisitionHistory = existingRecord.RequisitionHistory,
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(newRecord);
|
||||
}
|
||||
//push back any reminders
|
||||
if (existingRecord.ReminderRecordId != default)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(existingRecord.ReminderRecordId);
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
@@ -1781,6 +1988,8 @@ namespace CarCareTracker.Controllers
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
RequisitionHistory = result.RequisitionHistory,
|
||||
ReminderRecordId = result.ReminderRecordId,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_PlanRecordModal", convertedResult);
|
||||
@@ -1896,6 +2105,292 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
public IActionResult MoveRecords(List<int> recordIds, ImportMode source, ImportMode destination)
|
||||
{
|
||||
var genericRecord = new GenericRecord();
|
||||
bool result = false;
|
||||
foreach (int recordId in recordIds)
|
||||
{
|
||||
//get
|
||||
switch (source)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
genericRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
|
||||
break;
|
||||
}
|
||||
//save
|
||||
switch (destination)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord));
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(StaticHelper.GenericToRepairRecord(genericRecord));
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord));
|
||||
break;
|
||||
}
|
||||
//delete
|
||||
if (result)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
_serviceRecordDataAccess.DeleteServiceRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
_collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
_upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
public IActionResult DeleteRecords(List<int> recordIds, ImportMode importMode)
|
||||
{
|
||||
bool result = false;
|
||||
foreach (int recordId in recordIds)
|
||||
{
|
||||
switch (importMode)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
result = _serviceRecordDataAccess.DeleteServiceRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
result = _collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.GasRecord:
|
||||
result = _gasRecordDataAccess.DeleteGasRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.TaxRecord:
|
||||
result = _taxRecordDataAccess.DeleteTaxRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.SupplyRecord:
|
||||
result = _supplyRecordDataAccess.DeleteSupplyRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.NoteRecord:
|
||||
result = _noteDataAccess.DeleteNoteById(recordId);
|
||||
break;
|
||||
case ImportMode.OdometerRecord:
|
||||
result = _odometerRecordDataAccess.DeleteOdometerRecordById(recordId);
|
||||
break;
|
||||
case ImportMode.ReminderRecord:
|
||||
result = _reminderRecordDataAccess.DeleteReminderRecordById(recordId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
public IActionResult DuplicateRecords(List<int> recordIds, ImportMode importMode)
|
||||
{
|
||||
bool result = false;
|
||||
foreach (int recordId in recordIds)
|
||||
{
|
||||
switch (importMode)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
{
|
||||
var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
{
|
||||
var existingRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
{
|
||||
var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.GasRecord:
|
||||
{
|
||||
var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.TaxRecord:
|
||||
{
|
||||
var existingRecord = _taxRecordDataAccess.GetTaxRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.SupplyRecord:
|
||||
{
|
||||
var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.NoteRecord:
|
||||
{
|
||||
var existingRecord = _noteDataAccess.GetNoteById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _noteDataAccess.SaveNoteToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.OdometerRecord:
|
||||
{
|
||||
var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.ReminderRecord:
|
||||
{
|
||||
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(recordId);
|
||||
existingRecord.Id = default;
|
||||
result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetGenericRecordModal(List<int> recordIds, ImportMode dataType)
|
||||
{
|
||||
return PartialView("_GenericRecordModal", new GenericRecordEditModel() { DataType = dataType, RecordIds = recordIds });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult EditMultipleRecords(GenericRecordEditModel genericRecordEditModel)
|
||||
{
|
||||
var dateIsEdited = genericRecordEditModel.EditRecord.Date != default;
|
||||
var descriptionIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Description);
|
||||
var mileageIsEdited = genericRecordEditModel.EditRecord.Mileage != default;
|
||||
var costIsEdited = genericRecordEditModel.EditRecord.Cost != default;
|
||||
var noteIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Notes);
|
||||
var tagsIsEdited = genericRecordEditModel.EditRecord.Tags.Any();
|
||||
//handle clear overrides
|
||||
if (tagsIsEdited && genericRecordEditModel.EditRecord.Tags.Contains("---"))
|
||||
{
|
||||
genericRecordEditModel.EditRecord.Tags = new List<string>();
|
||||
}
|
||||
if (noteIsEdited && genericRecordEditModel.EditRecord.Notes == "---")
|
||||
{
|
||||
genericRecordEditModel.EditRecord.Notes = "";
|
||||
}
|
||||
bool result = false;
|
||||
foreach (int recordId in genericRecordEditModel.RecordIds)
|
||||
{
|
||||
switch (genericRecordEditModel.DataType)
|
||||
{
|
||||
case ImportMode.ServiceRecord:
|
||||
{
|
||||
var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
|
||||
if (dateIsEdited)
|
||||
{
|
||||
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
|
||||
}
|
||||
if (descriptionIsEdited)
|
||||
{
|
||||
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
|
||||
}
|
||||
if (mileageIsEdited)
|
||||
{
|
||||
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
|
||||
}
|
||||
if (costIsEdited)
|
||||
{
|
||||
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
|
||||
}
|
||||
if (noteIsEdited)
|
||||
{
|
||||
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
|
||||
}
|
||||
if (tagsIsEdited)
|
||||
{
|
||||
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
|
||||
}
|
||||
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.RepairRecord:
|
||||
{
|
||||
var existingRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
|
||||
if (dateIsEdited)
|
||||
{
|
||||
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
|
||||
}
|
||||
if (descriptionIsEdited)
|
||||
{
|
||||
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
|
||||
}
|
||||
if (mileageIsEdited)
|
||||
{
|
||||
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
|
||||
}
|
||||
if (costIsEdited)
|
||||
{
|
||||
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
|
||||
}
|
||||
if (noteIsEdited)
|
||||
{
|
||||
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
|
||||
}
|
||||
if (tagsIsEdited)
|
||||
{
|
||||
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
|
||||
}
|
||||
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
case ImportMode.UpgradeRecord:
|
||||
{
|
||||
var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
|
||||
if (dateIsEdited)
|
||||
{
|
||||
existingRecord.Date = genericRecordEditModel.EditRecord.Date;
|
||||
}
|
||||
if (descriptionIsEdited)
|
||||
{
|
||||
existingRecord.Description = genericRecordEditModel.EditRecord.Description;
|
||||
}
|
||||
if (mileageIsEdited)
|
||||
{
|
||||
existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage;
|
||||
}
|
||||
if (costIsEdited)
|
||||
{
|
||||
existingRecord.Cost = genericRecordEditModel.EditRecord.Cost;
|
||||
}
|
||||
if (noteIsEdited)
|
||||
{
|
||||
existingRecord.Notes = genericRecordEditModel.EditRecord.Notes;
|
||||
}
|
||||
if (tagsIsEdited)
|
||||
{
|
||||
existingRecord.Tags = genericRecordEditModel.EditRecord.Tags;
|
||||
}
|
||||
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
var existingRecord = GetExtraFieldsById(record.Id);
|
||||
string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb))";
|
||||
if (existingRecord.ExtraFields != null)
|
||||
{
|
||||
cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id";
|
||||
}
|
||||
string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb)) ON CONFLICT(id) DO UPDATE SET data = CAST(@data AS jsonb)";
|
||||
using (var ctext = pgDataSource.CreateCommand(cmd))
|
||||
{
|
||||
ctext.Parameters.AddWithValue("id", record.Id);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IConfigHelper
|
||||
{
|
||||
OpenIDConfig GetOpenIDConfig();
|
||||
UserConfig GetUserConfig(ClaimsPrincipal user);
|
||||
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
|
||||
string GetLogoUrl();
|
||||
@@ -28,6 +29,11 @@ namespace CarCareTracker.Helper
|
||||
_userConfig = userConfig;
|
||||
_cache = memoryCache;
|
||||
}
|
||||
public OpenIDConfig GetOpenIDConfig()
|
||||
{
|
||||
OpenIDConfig openIdConfig = _config.GetSection("OpenIDConfig").Get<OpenIDConfig>() ?? new OpenIDConfig();
|
||||
return openIdConfig;
|
||||
}
|
||||
public string GetLogoUrl()
|
||||
{
|
||||
var logoUrl = _config["LUBELOGGER_LOGO_URL"];
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IReportHelper
|
||||
{
|
||||
IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0);
|
||||
@@ -13,6 +14,21 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
public class ReportHelper: IReportHelper
|
||||
{
|
||||
public IEnumerable<CostForVehicleByMonth> GetOdometerRecordSum(List<OdometerRecord> odometerRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
odometerRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return odometerRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = 0,
|
||||
MaxMileage = x.Max(y => y.Mileage),
|
||||
MinMileage = x.Min(y => y.Mileage)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
@@ -23,7 +39,9 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
MaxMileage = x.Max(y=>y.Mileage),
|
||||
MinMileage = x.Min(y=>y.Mileage)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0)
|
||||
@@ -36,7 +54,9 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
MaxMileage = x.Max(y => y.Mileage),
|
||||
MinMileage = x.Min(y => y.Mileage)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0)
|
||||
@@ -49,7 +69,9 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
MaxMileage = x.Max(y => y.Mileage),
|
||||
MinMileage = x.Min(y => y.Mileage)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0)
|
||||
@@ -62,7 +84,9 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
Cost = x.Sum(y => y.Cost),
|
||||
MaxMileage = x.Max(y => y.Mileage),
|
||||
MinMileage = x.Min(y => y.Mileage)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using CarCareTracker.Models;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
@@ -130,7 +129,8 @@ namespace CarCareTracker.Helper
|
||||
Files = input.Files,
|
||||
Notes = input.Notes,
|
||||
Tags = input.Tags,
|
||||
ExtraFields = input.ExtraFields
|
||||
ExtraFields = input.ExtraFields,
|
||||
RequisitionHistory = input.RequisitionHistory
|
||||
};
|
||||
}
|
||||
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
|
||||
@@ -145,7 +145,8 @@ namespace CarCareTracker.Helper
|
||||
Files = input.Files,
|
||||
Notes = input.Notes,
|
||||
Tags = input.Tags,
|
||||
ExtraFields = input.ExtraFields
|
||||
ExtraFields = input.ExtraFields,
|
||||
RequisitionHistory = input.RequisitionHistory
|
||||
};
|
||||
}
|
||||
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
|
||||
@@ -160,7 +161,8 @@ namespace CarCareTracker.Helper
|
||||
Files = input.Files,
|
||||
Notes = input.Notes,
|
||||
Tags = input.Tags,
|
||||
ExtraFields = input.ExtraFields
|
||||
ExtraFields = input.ExtraFields,
|
||||
RequisitionHistory = input.RequisitionHistory
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,5 +212,9 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return fuelEconomyUnit;
|
||||
}
|
||||
public static long GetEpochFromDateTime(DateTime date)
|
||||
{
|
||||
return new DateTimeOffset(date).ToUnixTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -16,11 +14,13 @@ namespace CarCareTracker.Logic
|
||||
OperationResponse GenerateUserToken(string emailAddress, bool autoNotify);
|
||||
bool DeleteUserToken(int tokenId);
|
||||
bool DeleteUser(int userId);
|
||||
OperationResponse RegisterOpenIdUser(LoginModel credentials);
|
||||
OperationResponse RegisterNewUser(LoginModel credentials);
|
||||
OperationResponse RequestResetPassword(LoginModel credentials);
|
||||
OperationResponse ResetPasswordByUser(LoginModel credentials);
|
||||
OperationResponse ResetUserPassword(LoginModel credentials);
|
||||
UserData ValidateUserCredentials(LoginModel credentials);
|
||||
UserData ValidateOpenIDUser(LoginModel credentials);
|
||||
bool CheckIfUserIsValid(int userId);
|
||||
bool CreateRootUserCredentials(LoginModel credentials);
|
||||
bool DeleteRootUserCredentials();
|
||||
@@ -59,6 +59,45 @@ namespace CarCareTracker.Logic
|
||||
return result.Id != 0;
|
||||
}
|
||||
}
|
||||
public OperationResponse RegisterOpenIdUser(LoginModel credentials)
|
||||
{
|
||||
//validate their token.
|
||||
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
|
||||
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "Invalid Token" };
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName))
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "Username cannot be blank" };
|
||||
}
|
||||
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
|
||||
if (existingUser.Id != default)
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "Username already taken" };
|
||||
}
|
||||
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
|
||||
if (existingUserWithEmail.Id != default)
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
|
||||
}
|
||||
_tokenData.DeleteToken(existingToken.Id);
|
||||
var newUser = new UserData()
|
||||
{
|
||||
UserName = credentials.UserName,
|
||||
Password = GetHash(NewToken()), //generate a password for OpenID User
|
||||
EmailAddress = credentials.EmailAddress
|
||||
};
|
||||
var result = _userData.SaveUserRecord(newUser);
|
||||
if (result)
|
||||
{
|
||||
return new OperationResponse { Success = true, Message = "You will be logged in briefly." };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
|
||||
}
|
||||
}
|
||||
//handles user registration
|
||||
public OperationResponse RegisterNewUser(LoginModel credentials)
|
||||
{
|
||||
@@ -193,6 +232,19 @@ namespace CarCareTracker.Logic
|
||||
}
|
||||
}
|
||||
}
|
||||
public UserData ValidateOpenIDUser(LoginModel credentials)
|
||||
{
|
||||
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
|
||||
if (result.Id != default)
|
||||
{
|
||||
result.Password = string.Empty;
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UserData();
|
||||
}
|
||||
}
|
||||
#region "Admin Functions"
|
||||
public bool MakeUserAdmin(int userId, bool isAdmin)
|
||||
{
|
||||
@@ -280,19 +332,32 @@ namespace CarCareTracker.Logic
|
||||
#region "Root User"
|
||||
public bool CreateRootUserCredentials(LoginModel credentials)
|
||||
{
|
||||
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
|
||||
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||
if (existingUserConfig is not null)
|
||||
//check if file exists
|
||||
if (File.Exists(StaticHelper.UserConfigPath))
|
||||
{
|
||||
//create hashes of the login credentials.
|
||||
var hashedUserName = GetHash(credentials.UserName);
|
||||
var hashedPassword = GetHash(credentials.Password);
|
||||
//copy over settings that are off limits on the settings page.
|
||||
existingUserConfig.EnableAuth = true;
|
||||
existingUserConfig.UserNameHash = hashedUserName;
|
||||
existingUserConfig.UserPasswordHash = hashedPassword;
|
||||
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
|
||||
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||
if (existingUserConfig is not null)
|
||||
{
|
||||
//create hashes of the login credentials.
|
||||
var hashedUserName = GetHash(credentials.UserName);
|
||||
var hashedPassword = GetHash(credentials.Password);
|
||||
//copy over settings that are off limits on the settings page.
|
||||
existingUserConfig.EnableAuth = true;
|
||||
existingUserConfig.UserNameHash = hashedUserName;
|
||||
existingUserConfig.UserPasswordHash = hashedPassword;
|
||||
}
|
||||
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
||||
} else
|
||||
{
|
||||
var newUserConfig = new UserConfig()
|
||||
{
|
||||
EnableAuth = true,
|
||||
UserNameHash = GetHash(credentials.UserName),
|
||||
UserPasswordHash = GetHash(credentials.Password)
|
||||
};
|
||||
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig));
|
||||
}
|
||||
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
||||
_cache.Remove("userConfig_-1");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace CarCareTracker.Logic
|
||||
{
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using CarCareTracker.Logic;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
@@ -13,6 +14,20 @@
|
||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
public CollisionRecord ToCollisionRecord() { return new CollisionRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
Date = DateTime.Parse(Date),
|
||||
Cost = Cost,
|
||||
Mileage = Mileage,
|
||||
Description = Description,
|
||||
Notes = Notes,
|
||||
Files = Files,
|
||||
Tags = Tags,
|
||||
ExtraFields = ExtraFields,
|
||||
RequisitionHistory = RequisitionHistory
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
Models/GenericRecordEditModel.cs
Normal file
9
Models/GenericRecordEditModel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class GenericRecordEditModel
|
||||
{
|
||||
public ImportMode DataType { get; set; }
|
||||
public List<int> RecordIds { get; set; } = new List<int>();
|
||||
public GenericRecord EditRecord { get; set; } = new GenericRecord();
|
||||
}
|
||||
}
|
||||
16
Models/OIDC/OpenIDConfig.cs
Normal file
16
Models/OIDC/OpenIDConfig.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OpenIDConfig
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string AuthURL { get; set; }
|
||||
public string TokenURL { get; set; }
|
||||
public string RedirectURL { get; set; }
|
||||
public string Scope { get; set; }
|
||||
public string State { get; set; }
|
||||
public bool ValidateState { get; set; } = false;
|
||||
public string RemoteAuthURL { get { return $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; } }
|
||||
}
|
||||
}
|
||||
7
Models/OIDC/OpenIDResult.cs
Normal file
7
Models/OIDC/OpenIDResult.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OpenIDResult
|
||||
{
|
||||
public string id_token { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class PlanCostItem
|
||||
{
|
||||
public string CostName { get; set; }
|
||||
public decimal CostAmount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public DateTime DateCreated { get; set; }
|
||||
public DateTime DateModified { get; set; }
|
||||
public string Description { get; set; }
|
||||
@@ -14,5 +15,6 @@
|
||||
public PlanProgress Progress { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public string DateCreated { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string DateModified { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string Description { get; set; }
|
||||
@@ -15,9 +16,11 @@
|
||||
public PlanProgress Progress { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
public PlanRecord ToPlanRecord() { return new PlanRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
ReminderRecordId = ReminderRecordId,
|
||||
DateCreated = DateTime.Parse(DateCreated),
|
||||
DateModified = DateTime.Parse(DateModified),
|
||||
Description = Description,
|
||||
@@ -27,7 +30,12 @@
|
||||
Cost = Cost,
|
||||
Priority = Priority,
|
||||
Progress = Progress,
|
||||
ExtraFields = ExtraFields
|
||||
ExtraFields = ExtraFields,
|
||||
RequisitionHistory = RequisitionHistory
|
||||
}; }
|
||||
/// <summary>
|
||||
/// only used to hide view template button on plan create modal.
|
||||
/// </summary>
|
||||
public bool CreatedFromReminder { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
public int MonthId { get; set; }
|
||||
public string MonthName { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public int MaxMileage { get; set; }
|
||||
public int MinMileage { get; set; }
|
||||
public int DistanceTraveled { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
@@ -13,6 +14,20 @@
|
||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
public ServiceRecord ToServiceRecord() { return new ServiceRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
Date = DateTime.Parse(Date),
|
||||
Cost = Cost,
|
||||
Mileage = Mileage,
|
||||
Description = Description,
|
||||
Notes = Notes,
|
||||
Files = Files,
|
||||
Tags = Tags,
|
||||
ExtraFields = ExtraFields,
|
||||
RequisitionHistory = RequisitionHistory
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public List<string> Tags { get; set;} = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
}
|
||||
}
|
||||
@@ -35,5 +35,6 @@
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
public SupplyRecord ToSupplyRecord() { return new SupplyRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
@@ -26,7 +27,8 @@
|
||||
Notes = Notes,
|
||||
Files = Files,
|
||||
Tags = Tags,
|
||||
ExtraFields = ExtraFields
|
||||
ExtraFields = ExtraFields,
|
||||
RequisitionHistory = RequisitionHistory
|
||||
}; }
|
||||
}
|
||||
}
|
||||
|
||||
10
Models/Supply/SupplyUsageHistory.cs
Normal file
10
Models/Supply/SupplyUsageHistory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class SupplyUsageHistory {
|
||||
public DateTime Date { get; set; }
|
||||
public string PartNumber { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public int ReminderRecordId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
@@ -13,6 +14,20 @@
|
||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields }; }
|
||||
public List<SupplyUsageHistory> RequisitionHistory { get; set; } = new List<SupplyUsageHistory>();
|
||||
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
Date = DateTime.Parse(Date),
|
||||
Cost = Cost,
|
||||
Mileage = Mileage,
|
||||
Description = Description,
|
||||
Notes = Notes,
|
||||
Files = Files,
|
||||
Tags = Tags,
|
||||
ExtraFields = ExtraFields,
|
||||
RequisitionHistory = RequisitionHistory
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
README.md
80
README.md
@@ -1,39 +1,28 @@
|
||||

|
||||
|
||||
A self-hosted, open-source vehicle service records and maintainence tracker.
|
||||
Self-Hosted, Open-Source, Web-Based Vehicle Maintenance and Fuel Mileage Tracker
|
||||
|
||||
Visit our website: https://lubelogger.com
|
||||
|
||||
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
|
||||
Website: https://lubelogger.com
|
||||
|
||||
## Why
|
||||
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
|
||||
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintenance.
|
||||
|
||||
## Screenshots
|
||||
<a href="/docs/screenshots.md">Screenshots</a>
|
||||
## Showcase
|
||||
[Promotional Brochure](https://lubelogger.com/brochure.pdf)
|
||||
|
||||
[Screenshots](/docs/screenshots.md)
|
||||
|
||||
## Demo
|
||||
Try it out before you download it! The live demo resets every 20 minutes.
|
||||
|
||||
[Live Demo](https://demo.lubelogger.com) Login using username "test" and password "1234"
|
||||
|
||||
## Dependencies
|
||||
- Bootstrap
|
||||
- LiteDB
|
||||
- Bootstrap-DatePicker
|
||||
- SweetAlert2
|
||||
- CsvHelper
|
||||
- Chart.js
|
||||
- Drawdown
|
||||
## Download
|
||||
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
|
||||
|
||||
## Docker Setup (GHCR)
|
||||
1. Install Docker
|
||||
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
|
||||
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats. You can also setup SMTP Config here.
|
||||
4. If using traefik, use docker-compose.traefik.yml
|
||||
5. Run `docker-compose up`
|
||||
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
|
||||
|
||||
## Docker Setup (Manual Build)
|
||||
### Docker Setup (Manual Build for Advanced Users)
|
||||
1. Install Docker
|
||||
2. Clone this repo
|
||||
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
|
||||
@@ -42,38 +31,27 @@ Try it out before you download it! The live demo resets every 20 minutes.
|
||||
6. If using traefik, use docker-compose.traefik.yml
|
||||
7. Run `docker-compose up`
|
||||
|
||||
## Additional Docker Instructions
|
||||
### Need Help?
|
||||
[Documentation](https://docs.lubelogger.com/)
|
||||
|
||||
### manual
|
||||
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
|
||||
|
||||
- build
|
||||
[Search Existing Issues](https://github.com/hargata/lubelog/issues)
|
||||
|
||||
```
|
||||
docker build -t hargata/lubelog:latest .
|
||||
```
|
||||
## Dependencies
|
||||
- Bootstrap
|
||||
- LiteDB
|
||||
- Npgsql
|
||||
- Bootstrap-DatePicker
|
||||
- SweetAlert2
|
||||
- CsvHelper
|
||||
- Chart.js
|
||||
- Drawdown
|
||||
|
||||
- run
|
||||
## License
|
||||
LubeLogger utilizes a dual-licensing model, see [License](/LICENSE) for more information
|
||||
|
||||
```
|
||||
docker run -d hargata/lubelog:latest
|
||||
```
|
||||
## Support
|
||||
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
|
||||
|
||||
add `-v` for persistent volumes as needed. Have a look at the docker-compose.yml for examples.
|
||||
|
||||
## docker-compose
|
||||
|
||||
- build image
|
||||
|
||||
```
|
||||
docker compose build
|
||||
```
|
||||
|
||||
- run
|
||||
|
||||
```
|
||||
docker compose up
|
||||
|
||||
# or variant with traefik labels:
|
||||
|
||||
docker compose -f docker-compose.traefik.yml up
|
||||
```
|
||||
Note: Commercial users are required to maintain an active Patreon subscripton to be compliant with our licensing model.
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
@if(config.GetServerEnableShopSupplies())
|
||||
{
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
|
||||
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</button>
|
||||
<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>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@model List<ReminderRecordViewModel>
|
||||
@using CarCareTracker.Helper
|
||||
@model List<ReminderRecordViewModel>
|
||||
<script>
|
||||
var eventDates = [];
|
||||
var groupedDates = [];
|
||||
@foreach(ReminderRecordViewModel reminderRecord in Model)
|
||||
{
|
||||
@:eventDates.push({date: new Date(Date.parse(decodeHTMLEntities('@reminderRecord.Date.ToShortDateString()'))), description: decodeHTMLEntities('@reminderRecord.Description'), urgency: decodeHTMLEntities('@reminderRecord.Urgency.ToString()')});
|
||||
@:eventDates.push({ id: @reminderRecord.Id, date: decodeHTMLEntities('@StaticHelper.GetEpochFromDateTime(reminderRecord.Date)'), description: decodeHTMLEntities('@reminderRecord.Description'), urgency: decodeHTMLEntities('@reminderRecord.Urgency.ToString()') });
|
||||
}
|
||||
</script>
|
||||
<div class="row vehicleDetailTabContainer">
|
||||
@@ -13,6 +14,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="reminderRecordCalendarModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="reminderRecordCalendarModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
initCalendar();
|
||||
</script>
|
||||
@@ -23,9 +23,9 @@
|
||||
<div class="row gy-3 align-items-stretch vehiclesContainer">
|
||||
@foreach (Vehicle vehicle in Model)
|
||||
{
|
||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none garage-item" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
|
||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none garage-item" ondragover="dragOver(event)" ondrop="dropBox(event, @vehicle.Id)" draggable="true" ondragstart="dragStart(event, @vehicle.Id)" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
|
||||
<div class="card" onclick="viewVehicle(@vehicle.Id)">
|
||||
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
|
||||
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down; pointer-events:none;" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-truncate garage-item-year" data-unit="@vehicle.Year">@($"{vehicle.Year}")</h5>
|
||||
<h5 class="card-title text-truncate">@($"{vehicle.Make}")</h5>
|
||||
|
||||
39
Views/Home/_ReminderRecordCalendarModal.cshtml
Normal file
39
Views/Home/_ReminderRecordCalendarModal.cshtml
Normal file
@@ -0,0 +1,39 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model ReminderRecordViewModel
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(translator.Translate(userLanguage, "View Reminder"))</h5>
|
||||
<button type="button" class="btn-close" onclick="hideCalendarReminderModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12" id="reminderOptions">
|
||||
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
|
||||
<label for="reminderDescription">@translator.Translate(userLanguage, "Date")</label>
|
||||
<input type="text" id="reminderDescription" readonly class="form-control" value="@Model.Date.ToShortDateString()">
|
||||
<label for="reminderDescription">@translator.Translate(userLanguage,"Description")</label>
|
||||
<input type="text" id="reminderDescription" readonly class="form-control" value="@Model.Description">
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="reminderNotes">@translator.Translate(userLanguage,"Notes")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
|
||||
<textarea id="reminderNotes" readonly class="form-control" rows="5">@Model.Notes</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" onclick="deleteCalendarReminderRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="hideCalendarReminderModal()">@translator.Translate(userLanguage, "Close")</button>
|
||||
@if (Model.IsRecurring && (Model.Urgency == ReminderUrgency.VeryUrgent || Model.Urgency == ReminderUrgency.PastDue))
|
||||
{
|
||||
<button type="button" class="btn btn-primary" onclick="markDoneCalendarReminderRecord(@Model.Id)">@translator.Translate(userLanguage, "Mark as Done")</button>
|
||||
}
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<small class="text-body-secondary">Version 1.1.6</small>
|
||||
<small class="text-body-secondary">Version 1.2.3</small>
|
||||
</div>
|
||||
<p class="lead">
|
||||
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@{
|
||||
var logoUrl = config.GetLogoUrl();
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
var openIdConfigName = config.GetOpenIDConfig().Name;
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Login";
|
||||
@@ -31,6 +32,12 @@
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Login")</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(openIdConfigName))
|
||||
{
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-secondary mt-2" onclick="remoteLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@($"{translator.Translate(userLanguage, "Login via")} {openIdConfigName}")</button>
|
||||
</div>
|
||||
}
|
||||
<div class="d-grid">
|
||||
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
|
||||
</div>
|
||||
@@ -41,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function getRedirectURL(){
|
||||
function getRedirectURL() {
|
||||
return { url: decodeHTMLEntities('@Model') };
|
||||
}
|
||||
</script>
|
||||
50
Views/Login/OpenIDRegistration.cshtml
Normal file
50
Views/Login/OpenIDRegistration.cshtml
Normal file
@@ -0,0 +1,50 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@{
|
||||
var logoUrl = config.GetLogoUrl();
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
}
|
||||
@model string
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Register";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
|
||||
<input type="text" id="inputToken" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUserName" class="form-control" value="@Model">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="performOpenIdRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<a href="/Login/Index" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Back to Login")</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function performOpenIdRegistration() {
|
||||
var token = $("#inputToken").val();
|
||||
var userName = $("#inputUserName").val();
|
||||
var userEmail = decodeHTMLEntities('@Model');
|
||||
$.post('/Login/RegisterOpenIdUser', { userName: userName, token: token, emailAddress: userEmail }, function (data) {
|
||||
if (data.success) {
|
||||
successToast(data.message);
|
||||
setTimeout(function () { window.location.href = '/Home' }, 500);
|
||||
} else {
|
||||
errorToast(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -10,6 +10,7 @@
|
||||
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
|
||||
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
||||
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||
var firstDayOfWeek = (int)System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
|
||||
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
shortDatePattern = shortDatePattern.ToLower();
|
||||
@@ -58,7 +59,8 @@
|
||||
useMarkDown: "@useMarkDown" == "True",
|
||||
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
|
||||
useThreeDecimals: "@useThreeDecimals" == "True",
|
||||
useMPG: "@useMPG" == "True"
|
||||
useMPG: "@useMPG" == "True",
|
||||
firstDayOfWeek: @firstDayOfWeek
|
||||
}
|
||||
}
|
||||
function getShortDatePattern() {
|
||||
|
||||
@@ -156,6 +156,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="genericRecordEditModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="genericRecordEditModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="inputSuppliesModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="inputSuppliesModalContent"></div>
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
|
||||
<label for="collisionRecordDescription">@translator.Translate(userLanguage,"Description")</label>
|
||||
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) repaired(i.e. Alternator)")" value="@Model.Description">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('collisionRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<label for="collisionRecordCost">@translator.Translate(userLanguage,"Cost")</label>
|
||||
<input type="text" inputmode="decimal" id="collisionRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the repair")" value="@(isNew ? "" : Model.Cost)">
|
||||
@if (isNew)
|
||||
@@ -83,6 +91,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.RequisitionHistory.Any())
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
|
||||
}
|
||||
<div class="btn-group" style="margin-right:auto;">
|
||||
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteCollisionRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
|
||||
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -105,16 +117,18 @@
|
||||
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Repair Record")</button>
|
||||
}
|
||||
</div>
|
||||
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
var selectedSupplies = [];
|
||||
var recurringReminderRecordId = 0;
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
|
||||
}
|
||||
}
|
||||
}
|
||||
function getCollisionRecordModelData() {
|
||||
return { id: @Model.Id}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<tbody>
|
||||
@foreach (CollisionRecord collisionRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@collisionRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditCollisionRecordModal,@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@collisionRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
|
||||
@@ -87,4 +87,17 @@
|
||||
<div class="modal-content" id="collisionRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'RepairRecord', 'ServiceRecord')">@translator.Translate(userLanguage, "Service Records")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'RepairRecord', 'UpgradeRecord')">@translator.Translate(userLanguage, "Upgrades")</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'RepairRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -112,7 +112,7 @@
|
||||
<tbody>
|
||||
@foreach (GasRecordViewModel gasRecord in Model.GasRecords)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)" data-tags='@string.Join(" ", gasRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@gasRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditGasRecordModal,@gasRecord.Id)" data-tags='@string.Join(" ", gasRecord.Tags)'>
|
||||
<td class="col-2">@gasRecord.Date</td>
|
||||
<td class="col-2" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@gasRecord.Mileage</td>
|
||||
<td class="col-1">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
|
||||
@@ -136,6 +136,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'GasRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'GasRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
@if (!string.IsNullOrWhiteSpace(preferredFuelEconomyUnit))
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
|
||||
var sortedByMPG = Model.OrderBy(x => x.Cost).ToList();
|
||||
}
|
||||
@if (Model.Where(x=>x.Cost > 0).Any())
|
||||
@if (Model.Where(x=>x.Cost > 0).Any() || Model.Where(x=>x.DistanceTraveled > 0).Any())
|
||||
{
|
||||
<canvas id="bar-chart"></canvas>
|
||||
<script>
|
||||
@@ -16,6 +16,7 @@
|
||||
function renderChart() {
|
||||
var barGraphLabels = [];
|
||||
var barGraphData = [];
|
||||
var lineChartData = [];
|
||||
//color gradient from high to low
|
||||
var barGraphColors = [];
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
@@ -23,6 +24,7 @@
|
||||
{
|
||||
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||
@:lineChartData.push(globalParseFloat('@gasCost.DistanceTraveled'));
|
||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||
@:barGraphColors.push('@barGraphColors[index]');
|
||||
}
|
||||
@@ -34,7 +36,18 @@
|
||||
{
|
||||
label: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")'),
|
||||
backgroundColor: barGraphColors,
|
||||
data: barGraphData
|
||||
data: barGraphData,
|
||||
order: 1,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: decodeHTMLEntities('@translator.Translate(userLanguage, "Distance Traveled by Month")'),
|
||||
data: lineChartData,
|
||||
borderColor: useDarkMode ? "#fff" : "#000",
|
||||
backgroundColor: useDarkMode ? "#000" : "#fff",
|
||||
type: 'line',
|
||||
order: 0,
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +56,7 @@
|
||||
title: {
|
||||
display: true,
|
||||
color: useDarkMode ? "#fff" : "#000",
|
||||
text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses by Month")')
|
||||
text: decodeHTMLEntities('@translator.Translate(userLanguage, "Expenses and Distance Traveled by Month")')
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
@@ -59,6 +72,13 @@
|
||||
color: useDarkMode ? "#fff" : "#000"
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
position: 'right',
|
||||
ticks: {
|
||||
color: useDarkMode ? "#fff" : "#000"
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: useDarkMode ? "#fff" : "#000"
|
||||
|
||||
55
Views/Vehicle/_GenericRecordModal.cshtml
Normal file
55
Views/Vehicle/_GenericRecordModal.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model GenericRecordEditModel
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@translator.Translate(userLanguage,"Edit Multiple Records")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideGenericRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="genericRecordDate">@translator.Translate(userLanguage,"Date")</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="genericRecordDate" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
|
||||
</div>
|
||||
<label for="genericRecordMileage">@translator.Translate(userLanguage,"Odometer")</label>
|
||||
<input type="number" inputmode="numeric" id="genericRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<label for="genericRecordDescription">@translator.Translate(userLanguage, "Description")</label>
|
||||
<input type="text" id="genericRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<label for="genericRecordCost">@translator.Translate(userLanguage, "Cost")</label>
|
||||
<input type="text" inputmode="decimal" id="genericRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<label for="genericRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||
<select multiple class="form-select" id="genericRecordTag"></select>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="genericRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
|
||||
<textarea id="genericRecordNotes" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideGenericRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveGenericRecord()">@translator.Translate(userLanguage,"Edit")</button>
|
||||
</div>
|
||||
<script>
|
||||
var recordsToEdit = [];
|
||||
@foreach(int recordId in Model.RecordIds)
|
||||
{
|
||||
@:recordsToEdit.push(@recordId);
|
||||
}
|
||||
function getGenericRecordEditModelData(){
|
||||
return {
|
||||
dataType: decodeHTMLEntities('@Model.DataType.ToString()')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
@foreach (Note note in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)" data-tags='@string.Join(" ", note.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@note.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditNoteModal,@note.Id)" data-tags='@string.Join(" ", note.Tags)'>
|
||||
@if (note.Pinned)
|
||||
{
|
||||
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description</td>
|
||||
@@ -66,4 +66,16 @@
|
||||
<div class="modal-content" id="noteModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, true)">@translator.Translate(userLanguage, "Toggle Pin")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, false, true)">@translator.Translate(userLanguage, "Pin")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="pinNotes(selectedRow, false, false)">@translator.Translate(userLanguage, "Unpin")</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'NoteRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'NoteRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage,"# of Odometer Records")}: {Model.Count()}")</span>
|
||||
<span class="ms-2 badge bg-success" data-aggregate-type="count">@($"{translator.Translate(userLanguage, "# of Odometer Records")}: {Model.Count()}")</span>
|
||||
@foreach (string recordTag in recordTags)
|
||||
{
|
||||
<span onclick="filterTable('odometer-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||
@@ -28,21 +28,21 @@
|
||||
@if (enableCsvImports)
|
||||
{
|
||||
<div class="btn-group">
|
||||
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Odometer Record")</button>
|
||||
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Odometer Record")</button>
|
||||
<button type="button" class="btn btn-md btn-primary btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('OdometerRecord')">@translator.Translate(userLanguage,"Import via CSV")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">@translator.Translate(userLanguage,"Export to CSV")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('OdometerRecord')">@translator.Translate(userLanguage, "Import via CSV")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">@translator.Translate(userLanguage, "Export to CSV")</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage,"Print")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="printTab()">@translator.Translate(userLanguage, "Print")</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage,"Add Odometer Record")</button>
|
||||
<button onclick="showAddOdometerRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add Odometer Record")</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,15 +57,15 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage,"Date")</th>
|
||||
<th scope="col" class="col-3">@translator.Translate(userLanguage,"Odometer")</th>
|
||||
<th scope="col" class="col-7 col-xl-8">@translator.Translate(userLanguage,"Notes")</th>
|
||||
<th scope="col" class="col-2 col-xl-1">@translator.Translate(userLanguage, "Date")</th>
|
||||
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Odometer")</th>
|
||||
<th scope="col" class="col-7 col-xl-8">@translator.Translate(userLanguage, "Notes")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (OdometerRecord odometerRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditOdometerRecordModal(@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@odometerRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditOdometerRecordModal,@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1">@odometerRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-3" data-record-type="cost">@odometerRecord.Mileage</td>
|
||||
<td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
|
||||
@@ -82,4 +82,12 @@
|
||||
<div class="modal-content" id="odometerRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'OdometerRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'OdometerRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -16,7 +16,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-1">
|
||||
@if (Model.ReminderRecordId != default)
|
||||
{
|
||||
<div class="col-4 col-md-1">
|
||||
<i class="bi bi-bell"></i>
|
||||
</div>
|
||||
}
|
||||
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
|
||||
@if (Model.ImportMode == ImportMode.ServiceRecord)
|
||||
{
|
||||
<i class="bi bi-card-checklist"></i>
|
||||
@@ -30,7 +36,7 @@
|
||||
<i class="bi bi-exclamation-octagon"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="col-6 col-md-1">
|
||||
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
|
||||
@if (Model.Priority == PlanPriority.Critical)
|
||||
{
|
||||
<i class="bi bi-fire"></i>
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.RequisitionHistory.Any())
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
|
||||
}
|
||||
<button type="button" class="btn btn-danger" onclick="deletePlanRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary" onclick="hideAddPlanRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
@@ -97,7 +101,10 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="savePlanRecordTemplate()">@translator.Translate(userLanguage, "Save as Template")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
|
||||
@if (!Model.CreatedFromReminder)
|
||||
{
|
||||
<li><a class="dropdown-item" href="#" onclick="showPlanRecordTemplatesModal()">@translator.Translate(userLanguage, "View Templates")</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -106,6 +113,7 @@
|
||||
<button type="button" class="btn btn-primary" onclick="savePlanRecordToVehicle(true)">@translator.Translate(userLanguage, "Edit Plan Record")</button>
|
||||
}
|
||||
</div>
|
||||
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
var selectedSupplies = [];
|
||||
@@ -119,7 +127,9 @@
|
||||
function getPlanRecordModelData() {
|
||||
return {
|
||||
id: @Model.Id,
|
||||
dateCreated: decodeHTMLEntities('@(Model.DateCreated)')
|
||||
dateCreated: decodeHTMLEntities('@(Model.DateCreated)'),
|
||||
reminderRecordId: decodeHTMLEntities('@Model.ReminderRecordId'),
|
||||
createdFromReminder: @Model.CreatedFromReminder.ToString().ToLower()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -29,6 +29,10 @@
|
||||
<tr class="d-flex" id="supplyRows">
|
||||
<td class="col-8 text-truncate">
|
||||
@StaticHelper.TruncateStrings(planRecordTemplate.Description)
|
||||
@if(planRecordTemplate.ReminderRecordId != default)
|
||||
{
|
||||
<i class="bi bi-bell ms-2"></i>
|
||||
}
|
||||
@if (planRecordTemplate.Files.Any())
|
||||
{
|
||||
<i class="bi bi-paperclip ms-2"></i>
|
||||
|
||||
13
Views/Vehicle/_RecurringReminderSelector.cshtml
Normal file
13
Views/Vehicle/_RecurringReminderSelector.cshtml
Normal file
@@ -0,0 +1,13 @@
|
||||
@model List<ReminderRecord>
|
||||
<select class="form-select" id="recurringReminderInput">
|
||||
@if (Model.Any())
|
||||
{
|
||||
@foreach (ReminderRecord reminderRecord in Model)
|
||||
{
|
||||
<!option value="@reminderRecord.Id">@reminderRecord.Description</!option>
|
||||
}
|
||||
} else
|
||||
{
|
||||
<!option value="0">No Recurring Reminders Found</!option>
|
||||
}
|
||||
</select>
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="input-group">
|
||||
<input type="number" inputmode="numeric" id="reminderMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Future Odometer Reading")" value="@(isNew ? "" : Model.Mileage)">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="appendMileageToOdometer(500)">+500</button>
|
||||
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="appendMileageToOdometer(500)">+500</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
@@ -90,6 +90,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.IsRecurring)
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="createPlanRecordFromReminder(@Model.Id)">@translator.Translate(userLanguage, "Plan")</button>
|
||||
}
|
||||
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage, "Delete")</button>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary" onclick="hideAddReminderRecordModal()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<tbody>
|
||||
@foreach (ReminderRecordViewModel reminderRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditReminderRecordModal(@reminderRecord.Id)">
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@reminderRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditReminderRecordModal,@reminderRecord.Id)">
|
||||
@if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent)
|
||||
{
|
||||
<td class="col-1"><span class="badge text-bg-danger">@translator.Translate(userLanguage, "Very Urgent")</span></td>
|
||||
@@ -93,4 +93,18 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="planRecordModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="planRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ReminderRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -29,26 +29,55 @@
|
||||
<div class="col-md-6 col-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-1 d-sm-none d-md-block"></div>
|
||||
<div class="col-12 col-md-10 reportsCheckBoxContainer">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
|
||||
<label class="form-check-label" for="serviceExpenseCheck">@translator.Translate(userLanguage,"Service")</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
|
||||
<label class="form-check-label" for="repairExpenseCheck">@translator.Translate(userLanguage, "Repairs")</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
|
||||
<label class="form-check-label" for="upgradeExpenseCheck">@translator.Translate(userLanguage, "Upgrades")</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
|
||||
<label class="form-check-label" for="gasExpenseCheck">@translator.Translate(userLanguage, "Fuel")</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
|
||||
<label class="form-check-label" for="taxExpenseCheck">@translator.Translate(userLanguage, "Taxes")</label>
|
||||
<div class="col-12 col-md-10">
|
||||
<div class="dropdown d-grid dropdown-center">
|
||||
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
|
||||
Metrics
|
||||
</button>
|
||||
<ul class="dropdown-menu" style="width:100%;">
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input" onChange="updateCheckAll()" type="checkbox" id="selectAllExpenseCheck" value="0" checked>
|
||||
<label class="form-check-label stretched-link" for="selectAllExpenseCheck">@translator.Translate(userLanguage, "Select All")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="odometerExpenseCheck" value="6" checked>
|
||||
<label class="form-check-label stretched-link" for="odometerExpenseCheck">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
|
||||
<label class="form-check-label stretched-link" for="serviceExpenseCheck">@translator.Translate(userLanguage, "Service")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
|
||||
<label class="form-check-label stretched-link" for="repairExpenseCheck">@translator.Translate(userLanguage, "Repairs")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
|
||||
<label class="form-check-label stretched-link" for="upgradeExpenseCheck">@translator.Translate(userLanguage, "Upgrades")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
|
||||
<label class="form-check-label stretched-link" for="gasExpenseCheck">@translator.Translate(userLanguage, "Fuel")</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input reportCheckBox" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
|
||||
<label class="form-check-label stretched-link" for="taxExpenseCheck">@translator.Translate(userLanguage, "Taxes")</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 d-sm-none d-md-block"></div>
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
<input type="number" inputmode="numeric" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew ? "" : Model.Mileage)">
|
||||
<label for="serviceRecordDescription">@translator.Translate(userLanguage,"Description")</label>
|
||||
<input type="text" id="serviceRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) serviced(i.e. Oil Change)")" value="@Model.Description">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('serviceRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<label for="serviceRecordCost">@translator.Translate(userLanguage,"Cost")</label>
|
||||
<input type="text" inputmode="decimal" id="serviceRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the service")" value="@(isNew ? "" : Model.Cost)">
|
||||
@if (isNew)
|
||||
@@ -83,6 +91,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.RequisitionHistory.Any())
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
|
||||
}
|
||||
<div class="btn-group" style="margin-right:auto;">
|
||||
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteServiceRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
|
||||
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -105,9 +117,11 @@
|
||||
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Service Record")</button>
|
||||
}
|
||||
</div>
|
||||
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
var selectedSupplies = [];
|
||||
var recurringReminderRecordId = 0;
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<tbody>
|
||||
@foreach (ServiceRecord serviceRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditServiceRecordModal(@serviceRecord.Id)" data-tags='@string.Join(" ",serviceRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@serviceRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditServiceRecordModal,@serviceRecord.Id)" data-tags='@string.Join(" ", serviceRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1">@serviceRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@serviceRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@serviceRecord.Description</td>
|
||||
@@ -88,4 +88,17 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'ServiceRecord', 'RepairRecord')">@translator.Translate(userLanguage, "Repairs")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'ServiceRecord', 'UpgradeRecord')">@translator.Translate(userLanguage, "Upgrades")</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'ServiceRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -31,7 +31,12 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="supplyRecordQuantity">@translator.Translate(userLanguage,"Quantity")</label>
|
||||
<input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
|
||||
<div class="input-group">
|
||||
<input type="text" inputmode="decimal" id="supplyRecordQuantity" class="form-control" placeholder="@translator.Translate(userLanguage,"Quantity")" value="@(isNew ? "1" : Model.Quantity)">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm zero-y-padding btn-primary" onclick="replenishSupplies()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="supplyRecordCost">@translator.Translate(userLanguage,"Cost")</label>
|
||||
@@ -80,6 +85,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.RequisitionHistory.Any())
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-clock-history"></i></button>
|
||||
}
|
||||
<button type="button" class="btn btn-danger" onclick="deleteSupplyRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary" onclick="hideAddSupplyRecordModal()">@translator.Translate(userLanguage,"Cancel")</button>
|
||||
@@ -92,15 +101,16 @@
|
||||
<button type="button" class="btn btn-primary" onclick="saveSupplyRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Supply Record")</button>
|
||||
}
|
||||
</div>
|
||||
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
|
||||
}
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
|
||||
}
|
||||
}
|
||||
function getSupplyRecordModelData() {
|
||||
return { id: @Model.Id}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<tbody>
|
||||
@foreach (SupplyRecord supplyRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditSupplyRecordModal(@supplyRecord.Id)" data-tags='@string.Join(" ", supplyRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@supplyRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditSupplyRecordModal,@supplyRecord.Id)" data-tags='@string.Join(" ", supplyRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1">@supplyRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@supplyRecord.PartNumber</td>
|
||||
<td class="col-2">@supplyRecord.PartSupplier</td>
|
||||
@@ -91,4 +91,12 @@
|
||||
<div class="modal-content" id="supplyRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'SupplyRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'SupplyRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
72
Views/Vehicle/_SupplyRequisitionHistory.cshtml
Normal file
72
Views/Vehicle/_SupplyRequisitionHistory.cshtml
Normal file
@@ -0,0 +1,72 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
@model List<SupplyUsageHistory>
|
||||
<script>
|
||||
var supplyUsageHistory = [];
|
||||
</script>
|
||||
<div id="supplyUsageHistoryModalContainer" class="d-none">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@translator.Translate(userLanguage, "Supply Requisition History")</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12" style="max-height:50vh; overflow-y:auto;">
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Date")</th>
|
||||
@if(Model.Any(x=>!string.IsNullOrWhiteSpace(x.PartNumber))){
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Part Number")</th>
|
||||
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Description")</th>
|
||||
} else
|
||||
{
|
||||
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Description")</th>
|
||||
}
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Quantity")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Cost")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (SupplyUsageHistory usageHistory in Model)
|
||||
{
|
||||
<script>
|
||||
supplyUsageHistory.push({ date: decodeHTMLEntities("@usageHistory.Date.ToShortDateString()"), partNumber: decodeHTMLEntities('@usageHistory.PartNumber'), description: decodeHTMLEntities("@usageHistory.Description"), quantity: decodeHTMLEntities("@usageHistory.Quantity.ToString("F")"), cost: decodeHTMLEntities("@usageHistory.Cost.ToString("F")") })
|
||||
</script>
|
||||
<tr class="d-flex">
|
||||
<td class="col-2">@StaticHelper.TruncateStrings(usageHistory.Date.ToShortDateString())</td>
|
||||
@if (!string.IsNullOrWhiteSpace(usageHistory.PartNumber))
|
||||
{
|
||||
<td class="col-2 text-truncate">@StaticHelper.TruncateStrings(usageHistory.PartNumber)</td>
|
||||
<td class="col-4 text-truncate">@StaticHelper.TruncateStrings(usageHistory.Description)</td>
|
||||
} else
|
||||
{
|
||||
<td class="col-6 text-truncate">@StaticHelper.TruncateStrings(usageHistory.Description, 50)</td>
|
||||
}
|
||||
<td class="col-2">@usageHistory.Quantity.ToString("F")</td>
|
||||
<td class="col-2">@usageHistory.Cost.ToString("C2")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="text-center">
|
||||
<h4>@translator.Translate(userLanguage, "No supply requisitions in history")</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,6 +24,14 @@
|
||||
</div>
|
||||
<label for="taxRecordDescription">@translator.Translate(userLanguage,"Description")</label>
|
||||
<input type="text" id="taxRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of tax paid(i.e. Registration)")" value="@Model.Description">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('taxRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<label for="taxRecordCost">@translator.Translate(userLanguage,"Cost")</label>
|
||||
<input type="text" inputmode="decimal" id="taxRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of tax paid")" value="@(isNew? "" : Model.Cost)">
|
||||
<label for="taxRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
|
||||
@@ -107,6 +115,7 @@
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
var customMonthInterval = @Model.CustomMonthInterval;
|
||||
var recurringReminderRecordId = 0;
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<tbody>
|
||||
@foreach (TaxRecord taxRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)" data-tags='@string.Join(" ", taxRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@taxRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditTaxRecordModal,@taxRecord.Id)" data-tags='@string.Join(" ", taxRecord.Tags)'>
|
||||
<td class="col-3 col-xl-1">@taxRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-4 col-xl-6">@taxRecord.Description</td>
|
||||
<td class="col-2" data-record-type="cost">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
|
||||
@@ -85,4 +85,12 @@
|
||||
<div class="modal-content" id="taxRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'TaxRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'TaxRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
@@ -26,6 +26,14 @@
|
||||
<input type="number" inputmode="numeric" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew ? "" : Model.Mileage)">
|
||||
<label for="upgradeRecordDescription">@translator.Translate(userLanguage,"Description")</label>
|
||||
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Description of item(s) upgraded/modded")" value="@Model.Description">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('upgradeRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<label for="upgradeRecordCost">@translator.Translate(userLanguage,"Cost")</label>
|
||||
<input type="text" inputmode="decimal" id="upgradeRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"Cost of the upgrade/mods")" value="@(isNew ? "" : Model.Cost)">
|
||||
@if (isNew)
|
||||
@@ -83,6 +91,10 @@
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
@if (Model.RequisitionHistory.Any())
|
||||
{
|
||||
<button type="button" class="btn btn-warning" onclick="toggleSupplyUsageHistory()"><i class="bi bi-shop"></i></button>
|
||||
}
|
||||
<div class="btn-group" style="margin-right:auto;">
|
||||
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteUpgradeRecord(@Model.Id)">@translator.Translate(userLanguage,"Delete")</button>
|
||||
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -105,9 +117,11 @@
|
||||
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">@translator.Translate(userLanguage,"Edit Upgrade Record")</button>
|
||||
}
|
||||
</div>
|
||||
@await Html.PartialAsync("_SupplyRequisitionHistory", Model.RequisitionHistory)
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
var selectedSupplies = [];
|
||||
var recurringReminderRecordId = 0;
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<tbody>
|
||||
@foreach (UpgradeRecord upgradeRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@upgradeRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditUpgradeRecordModal,@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1">@upgradeRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@upgradeRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@upgradeRecord.Description</td>
|
||||
@@ -86,4 +86,17 @@
|
||||
<div class="modal-content" id="upgradeRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="table-context-menu dropdown-menu" style="display:none;">
|
||||
<li><a class="context-menu-multiple context-menu-select-all dropdown-item" href="#" onclick="selectAllRows()">@translator.Translate(userLanguage, "Select All")</a></li>
|
||||
<li><a class="context-menu-multiple context-menu-deselect-all dropdown-item" href="#" onclick="clearSelectedRows()">@translator.Translate(userLanguage, "Deselect All")</a></li>
|
||||
<li><a class="context-menu-active-multiple dropdown-item" href="#" onclick="editMultipleRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Edit Multiple")</a></li>
|
||||
<li><hr class="context-menu-multiple dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">@translator.Translate(userLanguage, "Move To")</h6></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'UpgradeRecord', 'ServiceRecord')">@translator.Translate(userLanguage, "Service Records")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="moveRecords(selectedRow, 'UpgradeRecord', 'RepairRecord')">@translator.Translate(userLanguage, "Repairs")</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Duplicate")</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="deleteRecords(selectedRow, 'UpgradeRecord')">@translator.Translate(userLanguage, "Delete")</a></li>
|
||||
</ul>
|
||||
BIN
docs/favicon.ico
Normal file
BIN
docs/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -2,6 +2,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<title>LubeLogger</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<style>
|
||||
@@ -30,7 +31,7 @@
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#features">Features</a></div>
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#demo">Demo</a></div>
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#download">Download</a></div>
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://docs.lubelogger.com">Docs</a></div>
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://docs.lubelogger.com">Documentation</a></div>
|
||||
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://github.com/hargata/lubelog">GitHub Repo</a></div>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -42,12 +43,12 @@
|
||||
<div class="row">
|
||||
<div id="carouselGallery" class="carousel slide">
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="0" class="active" aria-current="true"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="2"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="3"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="4"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="5"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="0" class="active" aria-current="true"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="2"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="3"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="4"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" style='background-color: #fff' data-bs-slide-to="5"></button>
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
@@ -94,11 +95,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselGallery" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="carousel-control-prev-icon" style='filter:inherit;' aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselGallery" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="carousel-control-next-icon" style='filter:inherit;' aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -141,7 +142,7 @@
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Live demo available <a href="https://demo.lubelogger.com">here</a>
|
||||
Live demo available <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://demo.lubelogger.com">here</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
@@ -151,44 +152,20 @@
|
||||
<hr>
|
||||
<div class="row" id="download">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h6 class="display-6 text-center">Where to Download</h6>
|
||||
<h6 class="display-6 text-center">Download</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
LubeLogger is released exclusively as a Docker Image via the GitHub Container Repository(GHCR)
|
||||
LubeLogger is available as both a Docker Image and a Windows Standalone Executable(EXE)
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
To pull down the Docker Image, run
|
||||
Read this <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://docs.lubelogger.com/Getting%20Started'>Getting Started Guide</a> on how to download either of them
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>docker pull ghcr.io/hargata/lubelogger:latest</code></p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Check the <a href="https://github.com/hargata/lubelog/" target="_blank">GitHub repository</a> and clone the "docker-compose.yml" and ".env" file onto your computer.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Navigate to the directory where those two files are located, then run
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>docker-compose up</code></p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Navigate to the URL the service is listening on, the default is
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>http://localhost:8080</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
@@ -196,15 +173,11 @@
|
||||
<h6 class="display-6 text-center">About</h6>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">LubeLogger is proudly developed in Price Utah by Hargata Softworks.
|
||||
<p class="lead">LubeLogger is proudly developed in <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://www.visitutah.com/Places-To-Go/Cities-and-Towns/Price" target="_blank">Price Utah</a> by <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='mailto:hargatasoftworks@gmail.com'>Hargata Softworks</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead"><a href="https://www.patreon.com/LubeLogger">Support us on Patreon</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">For more information regarding Price Utah, click <a href="https://www.visitutah.com/Places-To-Go/Cities-and-Towns/Price" target="_blank">here</a>
|
||||
<p class="lead"><a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href="https://www.patreon.com/LubeLogger">Support us on Patreon</a> or <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://buy.stripe.com/aEU9Egc8DdMc9bO144'>Donate on Stripe</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,6 +163,31 @@ html {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.tablerow-shake {
|
||||
animation: tablerowshake 1.2s cubic-bezier(.36, .07, .19, .97) both;
|
||||
backface-visibility: hidden;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@keyframes tablerowshake {
|
||||
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bellshake {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
@@ -270,6 +295,10 @@ html {
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.table-context-menu {
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -304,7 +333,29 @@ input[type="file"] {
|
||||
.override-hide{
|
||||
display: none !important;
|
||||
}
|
||||
.reminderCalendarViewContent + .datepicker, .datepicker-inline, .datepicker-days, .table-condensed {
|
||||
.reminderCalendarViewContent .datepicker, .reminderCalendarViewContent .datepicker-inline, .reminderCalendarViewContent .datepicker-days, .reminderCalendarViewContent .table-condensed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
table-layout: fixed;
|
||||
cursor: default;
|
||||
}
|
||||
.reminder-exist{
|
||||
overflow:auto;
|
||||
vertical-align:top;
|
||||
}
|
||||
.reminder-exist p{
|
||||
margin:0;
|
||||
}
|
||||
.reminderCalendarViewContent .datepicker table tr td.day {
|
||||
cursor: default;
|
||||
}
|
||||
.reminderCalendarViewContent .datepicker table tr td.day:hover {
|
||||
cursor: default;
|
||||
}
|
||||
.reminder-calendar-item{
|
||||
cursor: pointer;
|
||||
}
|
||||
.zero-y-padding{
|
||||
padding-top: 0rem;
|
||||
padding-bottom: 0rem;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -129,6 +129,8 @@ function getAndValidateCollisionRecordValues() {
|
||||
supplies: selectedSupplies,
|
||||
tags: collisionTags,
|
||||
addReminderRecord: addReminderRecord,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
requisitionHistory: supplyUsageHistory,
|
||||
reminderRecordId: recurringReminderRecordId
|
||||
}
|
||||
}
|
||||
@@ -62,30 +62,86 @@ function getVehicleCalendarEvents() {
|
||||
}
|
||||
});
|
||||
}
|
||||
function generateReminderItem(urgency, description) {
|
||||
function showCalendarReminderModal(id) {
|
||||
event.stopPropagation();
|
||||
$.get(`/Home/ViewCalendarReminder?reminderId=${id}`, function (data) {
|
||||
if (data) {
|
||||
$("#reminderRecordCalendarModalContent").html(data);
|
||||
$("#reminderRecordCalendarModal").modal('show');
|
||||
$('#reminderRecordCalendarModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
toggleMarkDownOverlay("reminderNotes");
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
function hideCalendarReminderModal() {
|
||||
$("#reminderRecordCalendarModal").modal('hide');
|
||||
}
|
||||
function generateReminderItem(id, urgency, description) {
|
||||
if (description.trim() == '') {
|
||||
return;
|
||||
}
|
||||
switch (urgency) {
|
||||
case "VeryUrgent":
|
||||
return `<p class="badge text-wrap bg-danger">${description}</p>`;
|
||||
return `<p class="badge text-wrap bg-danger reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
|
||||
case "PastDue":
|
||||
return `<p class="badge text-wrap bg-secondary">${description}</p>`;
|
||||
return `<p class="badge text-wrap bg-secondary reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
|
||||
case "Urgent":
|
||||
return `<p class="badge text-wrap bg-warning">${description}</p>`;
|
||||
return `<p class="badge text-wrap bg-warning reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
|
||||
case "NotUrgent":
|
||||
return `<p class="badge text-wrap bg-success">${description}</p>`;
|
||||
return `<p class="badge text-wrap bg-success reminder-calendar-item mb-2" onclick='showCalendarReminderModal(${id})'>${encodeHTMLInput(description)}</p>`;
|
||||
}
|
||||
}
|
||||
function markDoneCalendarReminderRecord(reminderRecordId, e) {
|
||||
event.stopPropagation();
|
||||
$.post(`/Vehicle/PushbackRecurringReminderRecord?reminderRecordId=${reminderRecordId}`, function (data) {
|
||||
if (data) {
|
||||
hideCalendarReminderModal();
|
||||
successToast("Reminder Updated");
|
||||
getVehicleCalendarEvents();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteCalendarReminderRecord(reminderRecordId, e) {
|
||||
if (e != undefined) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
$("#workAroundInput").show();
|
||||
Swal.fire({
|
||||
title: "Confirm Deletion?",
|
||||
text: "Deleted Reminders cannot be restored.",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post(`/Vehicle/DeleteReminderRecordById?reminderRecordId=${reminderRecordId}`, function (data) {
|
||||
if (data) {
|
||||
hideCalendarReminderModal();
|
||||
successToast("Reminder Deleted");
|
||||
getVehicleCalendarEvents();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
function initCalendar() {
|
||||
if (groupedDates.length == 0) {
|
||||
//group dates
|
||||
eventDates.map(x => {
|
||||
var existingIndex = groupedDates.findIndex(y => y.date.getTime() == x.date.getTime());
|
||||
var existingIndex = groupedDates.findIndex(y => y.date == x.date);
|
||||
if (existingIndex == -1) {
|
||||
groupedDates.push({ date: x.date, reminders: [`${generateReminderItem(x.urgency, x.description)}`] });
|
||||
groupedDates.push({ date: x.date, reminders: [`${generateReminderItem(x.id, x.urgency, x.description)}`] });
|
||||
} else if (existingIndex > -1) {
|
||||
groupedDates[existingIndex].reminders.push(`${generateReminderItem(x.urgency, x.description)}`);
|
||||
groupedDates[existingIndex].reminders.push(`${generateReminderItem(x.id, x.urgency, x.description)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -93,13 +149,14 @@ function initCalendar() {
|
||||
startDate: "+0d",
|
||||
format: getShortDatePattern().pattern,
|
||||
todayHighlight: true,
|
||||
weekStart: getGlobalConfig().firstDayOfWeek,
|
||||
beforeShowDay: function (date) {
|
||||
var reminderDateIndex = groupedDates.findIndex(x => x.date.getTime() == date.getTime());
|
||||
var reminderDateIndex = groupedDates.findIndex(x => (x.date == date.getTime() || x.date == (date.getTime() - date.getTimezoneOffset() * 60000))); //take into account server timezone offset
|
||||
if (reminderDateIndex > -1) {
|
||||
return {
|
||||
enabled: true,
|
||||
classes: 'reminder-exist',
|
||||
content: `<div class='text-wrap'><p>${date.getDate()}</p>${groupedDates[reminderDateIndex].reminders.join('<br>')}</div>`
|
||||
content: `<div class='text-wrap' style='height:20px;'><p>${date.getDate()}</p>${groupedDates[reminderDateIndex].reminders.join('<br>')}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,4 +306,48 @@ function sortGarage(sender, isMobile) {
|
||||
sortVehicles(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dragged = null;
|
||||
let draggedId = 0;
|
||||
function dragEnter(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
function dragStart(event, vehicleId) {
|
||||
dragged = event.target;
|
||||
draggedId = vehicleId;
|
||||
event.dataTransfer.setData('text/plain', draggedId);
|
||||
}
|
||||
function dragOver(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
function dropBox(event, targetVehicleId) {
|
||||
if (dragged.parentElement != event.target && event.target != dragged && draggedId != targetVehicleId) {
|
||||
copyContributors(draggedId, targetVehicleId);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
function copyContributors(sourceVehicleId, destVehicleId) {
|
||||
var sourceVehicleName = $(`#gridVehicle_${sourceVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" ");
|
||||
var destVehicleName = $(`#gridVehicle_${destVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" ");
|
||||
Swal.fire({
|
||||
title: "Copy Collaborators?",
|
||||
text: `Copy collaborators over from ${sourceVehicleName} to ${destVehicleName}?`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Copy",
|
||||
confirmButtonColor: "#0d6efd"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Vehicle/DuplicateVehicleCollaborators', { sourceVehicleId: sourceVehicleId, destVehicleId: destVehicleId }, function (data) {
|
||||
if (data.success) {
|
||||
successToast("Collaborators Copied");
|
||||
loadGarage();
|
||||
} else {
|
||||
errorToast(data.message);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -59,4 +59,12 @@ function handlePasswordKeyPress(event) {
|
||||
if (event.keyCode == 13) {
|
||||
performLogin();
|
||||
}
|
||||
}
|
||||
|
||||
function remoteLogin() {
|
||||
$.get('/Login/GetRemoteLoginLink', function (data) {
|
||||
if (data) {
|
||||
window.location.href = data;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -100,4 +100,12 @@ function getAndValidateNoteValues() {
|
||||
pinned: noteIsPinned,
|
||||
tags: noteTags
|
||||
}
|
||||
}
|
||||
function pinNotes(ids, toggle, pinStatus) {
|
||||
$.post('/Vehicle/PinNotes', { noteIds: ids, isToggle: toggle, pinStatus: pinStatus }, function (data) {
|
||||
if (data) {
|
||||
successToast(ids.length > 1 ? `${ids.length} Notes Updated` : "Note Updated.");
|
||||
getVehicleNotes(GetVehicleId().vehicleId);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -25,6 +25,10 @@ function showEditPlanRecordModal(planRecordId) {
|
||||
}
|
||||
function hideAddPlanRecordModal() {
|
||||
$('#planRecordModal').modal('hide');
|
||||
if (getPlanRecordModelData().createdFromReminder) {
|
||||
//show reminder Modal
|
||||
$("#reminderRecordModal").modal("show");
|
||||
}
|
||||
}
|
||||
function deletePlanRecord(planRecordId) {
|
||||
$("#workAroundInput").show();
|
||||
@@ -64,10 +68,12 @@ function savePlanRecordToVehicle(isEdit) {
|
||||
if (data) {
|
||||
successToast(isEdit ? "Plan Record Updated" : "Plan Record Added.");
|
||||
hideAddPlanRecordModal();
|
||||
saveScrollPosition();
|
||||
getVehiclePlanRecords(formValues.vehicleId);
|
||||
if (formValues.addReminderRecord) {
|
||||
setTimeout(function () { showAddReminderModal(formValues); }, 500);
|
||||
if (!getPlanRecordModelData().createdFromReminder) {
|
||||
saveScrollPosition();
|
||||
getVehiclePlanRecords(formValues.vehicleId);
|
||||
if (formValues.addReminderRecord) {
|
||||
setTimeout(function () { showAddReminderModal(formValues); }, 500);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
@@ -139,9 +145,6 @@ function savePlanRecordTemplate() {
|
||||
$.post('/Vehicle/SavePlanRecordTemplateToVehicleId', { planRecord: formValues }, function (data) {
|
||||
if (data.success) {
|
||||
successToast(data.message);
|
||||
hideAddPlanRecordModal();
|
||||
saveScrollPosition();
|
||||
getVehiclePlanRecords(formValues.vehicleId);
|
||||
} else {
|
||||
errorToast(data.message);
|
||||
}
|
||||
@@ -157,6 +160,7 @@ function getAndValidatePlanRecordValues() {
|
||||
var planDateCreated = getPlanRecordModelData().dateCreated;
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var planRecordId = getPlanRecordModelData().id;
|
||||
var reminderRecordId = getPlanRecordModelData().reminderRecordId;
|
||||
//validation
|
||||
var hasError = false;
|
||||
var extraFields = getAndValidateExtraFields();
|
||||
@@ -188,7 +192,9 @@ function getAndValidatePlanRecordValues() {
|
||||
priority: planPriority,
|
||||
progress: planProgress,
|
||||
importMode: planType,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
requisitionHistory: supplyUsageHistory,
|
||||
reminderRecordId: reminderRecordId
|
||||
}
|
||||
}
|
||||
//drag and drop stuff.
|
||||
|
||||
@@ -217,4 +217,26 @@ function getAndValidateReminderRecordValues() {
|
||||
customMileageInterval: customMileageInterval,
|
||||
customMonthInterval: customMonthInterval
|
||||
}
|
||||
}
|
||||
function createPlanRecordFromReminder(reminderRecordId) {
|
||||
//get values
|
||||
var formValues = getAndValidateReminderRecordValues();
|
||||
//validate
|
||||
if (formValues.hasError) {
|
||||
errorToast("Please check the form data");
|
||||
return;
|
||||
}
|
||||
var planModelInput = {
|
||||
id: 0,
|
||||
createdFromReminder: true,
|
||||
vehicleId: formValues.vehicleId,
|
||||
reminderRecordId: reminderRecordId,
|
||||
description: formValues.description,
|
||||
notes: formValues.notes
|
||||
};
|
||||
$.post('/Vehicle/GetAddPlanRecordPartialView', { planModel: planModelInput }, function (data) {
|
||||
$("#reminderRecordModal").modal("hide");
|
||||
$("#planRecordModalContent").html(data);
|
||||
$("#planRecordModal").modal("show");
|
||||
});
|
||||
}
|
||||
@@ -12,8 +12,15 @@ function generateVehicleHistoryReport() {
|
||||
}
|
||||
})
|
||||
}
|
||||
function updateCheckAll() {
|
||||
var isChecked = $("#selectAllExpenseCheck").is(":checked");
|
||||
$(".reportCheckBox").prop('checked', isChecked);
|
||||
setDebounce(refreshBarChart);
|
||||
}
|
||||
function updateCheck() {
|
||||
setDebounce(refreshBarChart);
|
||||
var allIsChecked = $(".reportCheckBox:checked").length == 6;
|
||||
$("#selectAllExpenseCheck").prop("checked", allIsChecked);
|
||||
}
|
||||
function refreshMPGChart() {
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
@@ -42,6 +49,9 @@ function refreshBarChart() {
|
||||
if ($("#taxExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('TaxRecord');
|
||||
}
|
||||
if ($("#odometerExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('OdometerRecord');
|
||||
}
|
||||
|
||||
$.post('/Vehicle/GetCostByMonthByVehicle',
|
||||
{
|
||||
|
||||
@@ -129,6 +129,8 @@ function getAndValidateServiceRecordValues() {
|
||||
supplies: selectedSupplies,
|
||||
tags: serviceTags,
|
||||
addReminderRecord: addReminderRecord,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
requisitionHistory: supplyUsageHistory,
|
||||
reminderRecordId: recurringReminderRecordId
|
||||
}
|
||||
}
|
||||
@@ -129,13 +129,15 @@ function initDatePicker(input, futureOnly) {
|
||||
input.datepicker({
|
||||
startDate: "+0d",
|
||||
format: getShortDatePattern().pattern,
|
||||
autoclose: true
|
||||
autoclose: true,
|
||||
weekStart: getGlobalConfig().firstDayOfWeek
|
||||
});
|
||||
} else {
|
||||
input.datepicker({
|
||||
endDate: "+0d",
|
||||
format: getShortDatePattern().pattern,
|
||||
autoclose: true
|
||||
autoclose: true,
|
||||
weekStart: getGlobalConfig().firstDayOfWeek
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -156,10 +158,15 @@ function hideMobileNav() {
|
||||
$(".lubelogger-mobile-nav").removeClass("lubelogger-mobile-nav-show");
|
||||
}
|
||||
function bindWindowResize() {
|
||||
$(window).resize(function () {
|
||||
$(window).on('resize', function () {
|
||||
hideMobileNav();
|
||||
});
|
||||
}
|
||||
function encodeHTMLInput(input) {
|
||||
const encoded = document.createElement('div');
|
||||
encoded.innerText = input;
|
||||
return encoded.innerHTML;
|
||||
}
|
||||
function decodeHTMLEntities(text) {
|
||||
return $("<textarea/>")
|
||||
.html(text)
|
||||
@@ -407,4 +414,454 @@ function getAndValidateExtraFields() {
|
||||
}
|
||||
});
|
||||
return { hasError: hasError, extraFields: outputData };
|
||||
}
|
||||
function toggleSupplyUsageHistory() {
|
||||
var container = $("#supplyUsageHistoryModalContainer");
|
||||
if (container.hasClass("d-none")) {
|
||||
container.removeClass("d-none");
|
||||
} else {
|
||||
container.addClass("d-none");
|
||||
}
|
||||
}
|
||||
function moveRecords(ids, source, dest) {
|
||||
if (ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
$("#workAroundInput").show();
|
||||
var friendlySource = "";
|
||||
var friendlyDest = "";
|
||||
var refreshDataCallBack;
|
||||
var recordVerbiage = ids.length > 1 ? `these ${ids.length} records` : "this record";
|
||||
switch (source) {
|
||||
case "ServiceRecord":
|
||||
friendlySource = "Service Records";
|
||||
refreshDataCallBack = getVehicleServiceRecords;
|
||||
break;
|
||||
case "RepairRecord":
|
||||
friendlySource = "Repairs";
|
||||
refreshDataCallBack = getVehicleCollisionRecords;
|
||||
break;
|
||||
case "UpgradeRecord":
|
||||
friendlySource = "Upgrades";
|
||||
refreshDataCallBack = getVehicleUpgradeRecords;
|
||||
break;
|
||||
}
|
||||
switch (dest) {
|
||||
case "ServiceRecord":
|
||||
friendlyDest = "Service Records";
|
||||
break;
|
||||
case "RepairRecord":
|
||||
friendlyDest = "Repairs";
|
||||
break;
|
||||
case "UpgradeRecord":
|
||||
friendlyDest = "Upgrades";
|
||||
break;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Confirm Move?",
|
||||
text: `Move ${recordVerbiage} from ${friendlySource} to ${friendlyDest}?`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Move",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Vehicle/MoveRecords', { recordIds: ids, source: source, destination: dest }, function (data) {
|
||||
if (data) {
|
||||
successToast(`${ids.length} Record(s) Moved`);
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
refreshDataCallBack(vehicleId);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteRecords(ids, source) {
|
||||
if (ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
$("#workAroundInput").show();
|
||||
var friendlySource = "";
|
||||
var refreshDataCallBack;
|
||||
var recordVerbiage = ids.length > 1 ? `these ${ids.length} records` : "this record";
|
||||
switch (source) {
|
||||
case "ServiceRecord":
|
||||
friendlySource = "Service Records";
|
||||
refreshDataCallBack = getVehicleServiceRecords;
|
||||
break;
|
||||
case "RepairRecord":
|
||||
friendlySource = "Repairs";
|
||||
refreshDataCallBack = getVehicleCollisionRecords;
|
||||
break;
|
||||
case "UpgradeRecord":
|
||||
friendlySource = "Upgrades";
|
||||
refreshDataCallBack = getVehicleUpgradeRecords;
|
||||
break;
|
||||
case "TaxRecord":
|
||||
friendlySource = "Taxes";
|
||||
refreshDataCallBack = getVehicleTaxRecords;
|
||||
break;
|
||||
case "SupplyRecord":
|
||||
friendlySource = "Supplies";
|
||||
refreshDataCallBack = getVehicleSupplyRecords;
|
||||
break;
|
||||
case "NoteRecord":
|
||||
friendlySource = "Notes";
|
||||
refreshDataCallBack = getVehicleNotes;
|
||||
break;
|
||||
case "OdometerRecord":
|
||||
friendlySource = "Odometer Records";
|
||||
refreshDataCallBack = getVehicleOdometerRecords;
|
||||
break;
|
||||
case "ReminderRecord":
|
||||
friendlySource = "Reminders";
|
||||
refreshDataCallBack = getVehicleReminders;
|
||||
break;
|
||||
case "GasRecord":
|
||||
friendlySource = "Fuel Records";
|
||||
refreshDataCallBack = getVehicleGasRecords;
|
||||
break;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Confirm Delete?",
|
||||
text: `Delete ${recordVerbiage} from ${friendlySource}?`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Vehicle/DeleteRecords', { recordIds: ids, importMode: source }, function (data) {
|
||||
if (data) {
|
||||
successToast(`${ids.length} Record(s) Deleted`);
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
refreshDataCallBack(vehicleId);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
function duplicateRecords(ids, source) {
|
||||
if (ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
$("#workAroundInput").show();
|
||||
var friendlySource = "";
|
||||
var refreshDataCallBack;
|
||||
var recordVerbiage = ids.length > 1 ? `these ${ids.length} records` : "this record";
|
||||
switch (source) {
|
||||
case "ServiceRecord":
|
||||
friendlySource = "Service Records";
|
||||
refreshDataCallBack = getVehicleServiceRecords;
|
||||
break;
|
||||
case "RepairRecord":
|
||||
friendlySource = "Repairs";
|
||||
refreshDataCallBack = getVehicleCollisionRecords;
|
||||
break;
|
||||
case "UpgradeRecord":
|
||||
friendlySource = "Upgrades";
|
||||
refreshDataCallBack = getVehicleUpgradeRecords;
|
||||
break;
|
||||
case "TaxRecord":
|
||||
friendlySource = "Taxes";
|
||||
refreshDataCallBack = getVehicleTaxRecords;
|
||||
break;
|
||||
case "SupplyRecord":
|
||||
friendlySource = "Supplies";
|
||||
refreshDataCallBack = getVehicleSupplyRecords;
|
||||
break;
|
||||
case "NoteRecord":
|
||||
friendlySource = "Notes";
|
||||
refreshDataCallBack = getVehicleNotes;
|
||||
break;
|
||||
case "OdometerRecord":
|
||||
friendlySource = "Odometer Records";
|
||||
refreshDataCallBack = getVehicleOdometerRecords;
|
||||
break;
|
||||
case "ReminderRecord":
|
||||
friendlySource = "Reminders";
|
||||
refreshDataCallBack = getVehicleReminders;
|
||||
break;
|
||||
case "GasRecord":
|
||||
friendlySource = "Fuel Records";
|
||||
refreshDataCallBack = getVehicleGasRecords;
|
||||
break;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Confirm Duplicate?",
|
||||
text: `Duplicate ${recordVerbiage}?`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Duplicate",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Vehicle/DuplicateRecords', { recordIds: ids, importMode: source }, function (data) {
|
||||
if (data) {
|
||||
successToast(`${ids.length} Record(s) Duplicated`);
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
refreshDataCallBack(vehicleId);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
var selectedRow = [];
|
||||
var isDragging = false;
|
||||
$(window).on('mouseup', function (e) {
|
||||
rangeMouseUp(e);
|
||||
});
|
||||
$(window).on('mousedown', function (e) {
|
||||
rangeMouseDown(e);
|
||||
});
|
||||
$(window).on('keydown', function (e) {
|
||||
var userOnInput = $(e.target).is("input") || $(e.target).is("textarea");
|
||||
if (!userOnInput) {
|
||||
if (e.ctrlKey && e.which == 65) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectAllRows();
|
||||
}
|
||||
}
|
||||
})
|
||||
function selectAllRows() {
|
||||
clearSelectedRows();
|
||||
$('.vehicleDetailTabContainer .table tbody tr:visible').addClass('table-active');
|
||||
$('.vehicleDetailTabContainer .table tbody tr:visible').map((index, elem) => {
|
||||
addToSelectedRows($(elem).attr('data-rowId'));
|
||||
});
|
||||
}
|
||||
function rangeMouseDown(e) {
|
||||
if (isRightClick(e)) {
|
||||
return;
|
||||
}
|
||||
var contextMenuAction = $(e.target).is(".table-context-menu > li > .dropdown-item")
|
||||
if (!e.ctrlKey && !contextMenuAction) {
|
||||
clearSelectedRows();
|
||||
}
|
||||
isDragging = true;
|
||||
|
||||
document.documentElement.onselectstart = function () { return false; };
|
||||
}
|
||||
function isRightClick(e) {
|
||||
if (e.which) {
|
||||
return (e.which == 3);
|
||||
} else if (e.button) {
|
||||
return (e.button == 2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function stopEvent() {
|
||||
event.stopPropagation();
|
||||
}
|
||||
function rangeMouseUp(e) {
|
||||
if ($(".table-context-menu").length > 0) {
|
||||
$(".table-context-menu").hide();
|
||||
}
|
||||
if (isRightClick(e)) {
|
||||
return;
|
||||
}
|
||||
isDragging = false;
|
||||
document.documentElement.onselectstart = function () { return true; };
|
||||
}
|
||||
function rangeMouseMove(e) {
|
||||
if (isDragging) {
|
||||
if (!$(e).hasClass('table-active')) {
|
||||
addToSelectedRows($(e).attr('data-rowId'));
|
||||
$(e).addClass('table-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
function addToSelectedRows(id) {
|
||||
if (selectedRow.findIndex(x => x == id) == -1) {
|
||||
selectedRow.push(id);
|
||||
}
|
||||
}
|
||||
function removeFromSelectedRows(id) {
|
||||
var rowIndex = selectedRow.findIndex(x => x == id)
|
||||
if (rowIndex != -1) {
|
||||
selectedRow.splice(rowIndex, 1);
|
||||
}
|
||||
}
|
||||
function clearSelectedRows() {
|
||||
selectedRow = [];
|
||||
$('.table tr').removeClass('table-active');
|
||||
}
|
||||
function getDeviceIsTouchOnly() {
|
||||
if (navigator.maxTouchPoints > 0 && matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function showTableContextMenu(e) {
|
||||
if (event != undefined) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (getDeviceIsTouchOnly()) {
|
||||
return;
|
||||
}
|
||||
$(".table-context-menu").show();
|
||||
determineContextMenuItems();
|
||||
$(".table-context-menu").css({
|
||||
position: "absolute",
|
||||
left: getMenuPosition(event.clientX, 'width', 'scrollLeft'),
|
||||
top: getMenuPosition(event.clientY, 'height', 'scrollTop')
|
||||
});
|
||||
if (!$(e).hasClass('table-active')) {
|
||||
clearSelectedRows();
|
||||
addToSelectedRows($(e).attr('data-rowId'));
|
||||
$(e).addClass('table-active');
|
||||
}
|
||||
}
|
||||
function determineContextMenuItems() {
|
||||
var tableRows = $('.table tbody tr:visible');
|
||||
var tableRowsActive = $('.table tr.table-active');
|
||||
if (tableRowsActive.length == 1) {
|
||||
//only one row selected
|
||||
$(".context-menu-active-single").show();
|
||||
$(".context-menu-active-multiple").hide();
|
||||
} else if (tableRowsActive.length > 1) {
|
||||
//multiple rows selected
|
||||
$(".context-menu-active-single").hide();
|
||||
$(".context-menu-active-multiple").show();
|
||||
} else {
|
||||
//nothing was selected, bug case.
|
||||
$(".context-menu-active-single").hide();
|
||||
$(".context-menu-active-multiple").hide();
|
||||
}
|
||||
if (tableRows.length > 1) {
|
||||
$(".context-menu-multiple").show();
|
||||
if (tableRows.length == tableRowsActive.length) {
|
||||
//all rows are selected, show deselect all button.
|
||||
$(".context-menu-deselect-all").show();
|
||||
$(".context-menu-select-all").hide();
|
||||
} else if (tableRows.length != tableRowsActive.length) {
|
||||
//not all rows are selected, show select all button.
|
||||
$(".context-menu-select-all").show();
|
||||
$(".context-menu-deselect-all").hide();
|
||||
}
|
||||
} else {
|
||||
$(".context-menu-multiple").hide();
|
||||
}
|
||||
}
|
||||
function getMenuPosition(mouse, direction, scrollDir) {
|
||||
var win = $(window)[direction](),
|
||||
scroll = $(window)[scrollDir](),
|
||||
menu = $(".table-context-menu")[direction](),
|
||||
position = mouse + scroll;
|
||||
|
||||
// opening menu would pass the side of the page
|
||||
if (mouse + menu > win && menu < mouse)
|
||||
position -= menu;
|
||||
return position;
|
||||
}
|
||||
function handleTableRowClick(e, callBack, rowId) {
|
||||
if (!event.ctrlKey) {
|
||||
callBack(rowId);
|
||||
} else if (!$(e).hasClass('table-active')) {
|
||||
addToSelectedRows($(e).attr('data-rowId'));
|
||||
$(e).addClass('table-active');
|
||||
} else if ($(e).hasClass('table-active')) {
|
||||
removeFromSelectedRows($(e).attr('data-rowId'));
|
||||
$(e).removeClass('table-active');
|
||||
}
|
||||
}
|
||||
|
||||
function showTableContextMenuForMobile(e, xPosition, yPosition) {
|
||||
if (!$(e).hasClass('table-active')) {
|
||||
addToSelectedRows($(e).attr('data-rowId'));
|
||||
$(e).addClass('table-active');
|
||||
shakeTableRow(e);
|
||||
} else {
|
||||
$(".table-context-menu").show();
|
||||
determineContextMenuItems();
|
||||
$(".table-context-menu").css({
|
||||
position: "absolute",
|
||||
left: getMenuPosition(xPosition, 'width', 'scrollLeft'),
|
||||
top: getMenuPosition(yPosition, 'height', 'scrollTop')
|
||||
});
|
||||
}
|
||||
}
|
||||
function shakeTableRow(e) {
|
||||
$(e).addClass('tablerow-shake');
|
||||
setTimeout(function () { $(e).removeClass('tablerow-shake'); }, 1200)
|
||||
}
|
||||
var rowTouchTimer;
|
||||
var rowTouchDuration = 800;
|
||||
function detectRowLongTouch(sender) {
|
||||
var touchX = event.touches[0].clientX;
|
||||
var touchY = event.touches[0].clientY;
|
||||
if (!rowTouchTimer) {
|
||||
rowTouchTimer = setTimeout(function () { showTableContextMenuForMobile(sender, touchX, touchY); detectRowTouchEndPremature(sender); }, rowTouchDuration);
|
||||
}
|
||||
}
|
||||
function detectRowTouchEndPremature(sender) {
|
||||
if (rowTouchTimer) {
|
||||
clearTimeout(rowTouchTimer);
|
||||
rowTouchTimer = null;
|
||||
}
|
||||
}
|
||||
function replenishSupplies() {
|
||||
Swal.fire({
|
||||
title: 'Replenish Supplies',
|
||||
html: `
|
||||
<input type="text" id="inputSupplyAddQuantity" class="swal2-input" placeholder="Quantity">
|
||||
<br />
|
||||
<input type="text" id="inputSupplyAddCost" class="swal2-input" placeholder="Cost">
|
||||
<br />
|
||||
<span class='small'>leave blank to use unit cost calculation</span>
|
||||
`,
|
||||
confirmButtonText: 'Replenish',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const replquantity = globalParseFloat($("#inputSupplyAddQuantity").val());
|
||||
const replcost = $("#inputSupplyAddCost").val();
|
||||
const parsedReplCost = globalParseFloat(replcost);
|
||||
var quantitybeforeRepl = globalParseFloat($('#supplyRecordQuantity').val());
|
||||
if (isNaN(replquantity) || (replcost.trim() != '' && isNaN(parsedReplCost))) {
|
||||
Swal.showValidationMessage(`Please enter a valid quantity and cost`);
|
||||
} else if (replcost.trim() == '' && (isNaN(quantitybeforeRepl) || quantitybeforeRepl == 0)){
|
||||
Swal.showValidationMessage(`Unable to use unit cost calculation, please provide cost`);
|
||||
}
|
||||
return { replquantity, replcost, parsedReplCost }
|
||||
},
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
var replenishedCost = result.value.replcost;
|
||||
var parsedReplenishedCost = result.value.parsedReplCost;
|
||||
var replenishedQuantity = result.value.replquantity;
|
||||
var currentCost = globalParseFloat($('#supplyRecordCost').val())
|
||||
if (isNaN(currentCost)) {
|
||||
currentCost = 0;
|
||||
}
|
||||
var currentQuantity = globalParseFloat($('#supplyRecordQuantity').val());
|
||||
var newQuantity = currentQuantity + replenishedQuantity;
|
||||
if (replenishedCost.trim() == '') {
|
||||
|
||||
var unitCost = currentCost / currentQuantity;
|
||||
var newCost = newQuantity * unitCost;
|
||||
//set text fields.
|
||||
$('#supplyRecordCost').val(globalFloatToString(newCost.toFixed(3).toString()));
|
||||
$('#supplyRecordQuantity').val(globalFloatToString(newQuantity.toFixed(3).toString()));
|
||||
} else {
|
||||
var newCost = currentCost + parsedReplenishedCost;
|
||||
//set text fields.
|
||||
$('#supplyRecordCost').val(globalFloatToString(newCost.toFixed(3).toString()));
|
||||
$('#supplyRecordQuantity').val(globalFloatToString(newQuantity.toFixed(3).toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -130,6 +130,7 @@ function getAndValidateSupplyRecordValues() {
|
||||
quantity: supplyQuantity,
|
||||
files: uploadedFiles,
|
||||
tags: supplyTags,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
requisitionHistory: supplyUsageHistory
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ function getAndValidateTaxRecordValues() {
|
||||
tags: taxTags,
|
||||
files: uploadedFiles,
|
||||
addReminderRecord: addReminderRecord,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
reminderRecordId: recurringReminderRecordId
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,8 @@ function getAndValidateUpgradeRecordValues() {
|
||||
supplies: selectedSupplies,
|
||||
tags: upgradeTags,
|
||||
addReminderRecord: addReminderRecord,
|
||||
extraFields: extraFields.extraFields
|
||||
extraFields: extraFields.extraFields,
|
||||
requisitionHistory: supplyUsageHistory,
|
||||
reminderRecordId: recurringReminderRecordId
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,7 @@ function deleteVehicle(vehicleId) {
|
||||
}
|
||||
function showAddReminderModal(reminderModalInput) {
|
||||
if (reminderModalInput != undefined) {
|
||||
$.post('/Vehicle/GetAddReminderRecordPartialView', {reminderModel: reminderModalInput}, function (data) {
|
||||
$.post('/Vehicle/GetAddReminderRecordPartialView', { reminderModel: reminderModalInput }, function (data) {
|
||||
$("#reminderRecordModalContent").html(data);
|
||||
initDatePicker($('#reminderDate'), true);
|
||||
$("#reminderRecordModal").modal("show");
|
||||
@@ -313,7 +313,7 @@ function moveRecord(recordId, source, dest) {
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Vehicle/MoveRecord', {recordId: recordId, source: source, destination: dest }, function (data) {
|
||||
$.post('/Vehicle/MoveRecord', { recordId: recordId, source: source, destination: dest }, function (data) {
|
||||
if (data) {
|
||||
hideModalCallBack();
|
||||
successToast("Record Moved");
|
||||
@@ -327,4 +327,115 @@ function moveRecord(recordId, source, dest) {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
function showRecurringReminderSelector(descriptionFieldName) {
|
||||
$.get(`/Vehicle/GetRecurringReminderRecordsByVehicleId?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
|
||||
if (data) {
|
||||
//prompt user to select a recurring reminder
|
||||
Swal.fire({
|
||||
title: 'Select Recurring Reminder',
|
||||
html: data,
|
||||
confirmButtonText: 'Select',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const selectedRecurringReminder = $("#recurringReminderInput").val();
|
||||
const selectedRecurringReminderText = $("#recurringReminderInput option:selected").text();
|
||||
if (!selectedRecurringReminder || parseInt(selectedRecurringReminder) == 0) {
|
||||
Swal.showValidationMessage(`You must select a recurring reminder`);
|
||||
}
|
||||
return { selectedRecurringReminder, selectedRecurringReminderText }
|
||||
},
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
recurringReminderRecordId = result.value.selectedRecurringReminder;
|
||||
var descriptionField = $(`#${descriptionFieldName}`);
|
||||
if (descriptionField.length > 0) {
|
||||
descriptionField.val(result.value.selectedRecurringReminderText.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
})
|
||||
}
|
||||
function editMultipleRecords(ids, dataType) {
|
||||
$.post('/Vehicle/GetGenericRecordModal', { recordIds: ids, dataType: dataType }, function (data) {
|
||||
if (data) {
|
||||
$("#genericRecordEditModalContent").html(data);
|
||||
initDatePicker($('#genericRecordDate'));
|
||||
initTagSelector($("#genericRecordTag"));
|
||||
$("#genericRecordEditModal").modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
function hideGenericRecordModal() {
|
||||
$("#genericRecordEditModal").modal('hide');
|
||||
}
|
||||
function saveGenericRecord() {
|
||||
//get values
|
||||
var formValues = getAndValidateGenericRecordValues();
|
||||
//validate
|
||||
if (formValues.hasError) {
|
||||
errorToast("Please check the form data");
|
||||
return;
|
||||
}
|
||||
var refreshDataCallBack;
|
||||
switch (formValues.dataType) {
|
||||
case "ServiceRecord":
|
||||
refreshDataCallBack = getVehicleServiceRecords;
|
||||
break;
|
||||
case "RepairRecord":
|
||||
refreshDataCallBack = getVehicleCollisionRecords;
|
||||
break;
|
||||
case "UpgradeRecord":
|
||||
refreshDataCallBack = getVehicleUpgradeRecords;
|
||||
break;
|
||||
}
|
||||
//save to db.
|
||||
$.post('/Vehicle/EditMultipleRecords', { genericRecordEditModel: formValues }, function (data) {
|
||||
if (data) {
|
||||
successToast(formValues.recordIds.length > 1 ? "Records Updated" : "Record Updated.");
|
||||
hideGenericRecordModal();
|
||||
refreshDataCallBack(GetVehicleId().vehicleId);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
})
|
||||
}
|
||||
function getAndValidateGenericRecordValues() {
|
||||
var genericDate = $("#genericRecordDate").val();
|
||||
var genericMileage = $("#genericRecordMileage").val();
|
||||
var genericMileageToParse = parseInt(globalParseFloat($("#genericRecordMileage").val())).toString();
|
||||
var genericDescription = $("#genericRecordDescription").val();
|
||||
var genericCost = $("#genericRecordCost").val();
|
||||
var genericNotes = $("#genericRecordNotes").val();
|
||||
var genericTags = $("#genericRecordTag").val();
|
||||
//validation
|
||||
var hasError = false;
|
||||
if (genericMileage.trim() != '' && (isNaN(genericMileageToParse) || parseInt(genericMileageToParse) < 0)) {
|
||||
hasError = true;
|
||||
$("#genericRecordMileage").addClass("is-invalid");
|
||||
} else {
|
||||
$("#genericRecordMileage").removeClass("is-invalid");
|
||||
}
|
||||
if (genericCost.trim() != '' && !isValidMoney(genericCost)) {
|
||||
hasError = true;
|
||||
$("#genericRecordCost").addClass("is-invalid");
|
||||
} else {
|
||||
$("#genericRecordCost").removeClass("is-invalid");
|
||||
}
|
||||
return {
|
||||
hasError: hasError,
|
||||
dataType: getGenericRecordEditModelData().dataType,
|
||||
recordIds: recordsToEdit,
|
||||
editRecord: {
|
||||
date: genericDate,
|
||||
mileage: genericMileageToParse,
|
||||
description: genericDescription,
|
||||
cost: genericCost,
|
||||
notes: genericNotes,
|
||||
tags: genericTags
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,8 +299,7 @@
|
||||
if (self.$element.attr('disabled')) {
|
||||
self.$input.attr('disabled', 'disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
switch (event.which) {
|
||||
// BACKSPACE
|
||||
case 8:
|
||||
@@ -339,7 +338,38 @@
|
||||
$nextTag.after($inputWrapper);
|
||||
$input.focus();
|
||||
}
|
||||
break;
|
||||
break;
|
||||
//COPY EVENT
|
||||
case 67:
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
navigator.clipboard.writeText(self.itemsArray.join(" "));
|
||||
}
|
||||
break;
|
||||
//PASTE EVENT
|
||||
case 86:
|
||||
if (event.ctrlKey) {
|
||||
setTimeout(function () {
|
||||
var pastedString = $input.val();
|
||||
//clear pasted string.
|
||||
$input.val('');
|
||||
//process input one by one.
|
||||
if (pastedString.length > 0) {
|
||||
var tagsToAdd = pastedString.split(" ");
|
||||
tagsToAdd.forEach(x => {
|
||||
self.add(x);
|
||||
})
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
break;
|
||||
//ENTER EVENT
|
||||
case 13:
|
||||
case 32:
|
||||
var tagToAdd = $input.val();
|
||||
$input.val('');
|
||||
self.add(tagToAdd);
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
@@ -351,61 +381,18 @@
|
||||
$input.attr('size', Math.max(this.inputSize, size));
|
||||
}, self));
|
||||
|
||||
self.$container.on('input', 'input', $.proxy(function (event) {
|
||||
if (event.originalEvent.data == undefined) {
|
||||
var $input = $(event.target);
|
||||
var text = $input.val(),
|
||||
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
||||
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
||||
//check if confirm keys are in input and then replace them.
|
||||
text = text.replace(String.fromCharCode(event.which), "")
|
||||
// Only attempt to add a tag if there is data in the field
|
||||
if (text.length !== 0) {
|
||||
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
||||
$input.val('');
|
||||
}
|
||||
|
||||
// If the field is empty, let the event triggered fire as usual
|
||||
if (self.options.cancelConfirmKeysOnEmpty === false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
var textLength = $input.val().length,
|
||||
wordSpace = Math.ceil(textLength / 5),
|
||||
size = textLength + wordSpace + 1;
|
||||
$input.attr('size', Math.max(this.inputSize, size));
|
||||
};
|
||||
}));
|
||||
|
||||
self.$container.on('keypress', 'input', $.proxy(function(event) {
|
||||
var $input = $(event.target);
|
||||
|
||||
if (self.$element.attr('disabled')) {
|
||||
self.$input.attr('disabled', 'disabled');
|
||||
return;
|
||||
}
|
||||
var text = $input.val(),
|
||||
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
||||
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
||||
//check if confirm keys are in input and then replace them.
|
||||
text = text.replace(String.fromCharCode(event.which), "")
|
||||
// Only attempt to add a tag if there is data in the field
|
||||
if (text.length !== 0) {
|
||||
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
||||
$input.val('');
|
||||
}
|
||||
|
||||
// If the field is empty, let the event triggered fire as usual
|
||||
if (self.options.cancelConfirmKeysOnEmpty === false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset internal input's size
|
||||
var textLength = $input.val().length,
|
||||
wordSpace = Math.ceil(textLength / 5),
|
||||
size = textLength + wordSpace + 1;
|
||||
$input.attr('size', Math.max(this.inputSize, size));
|
||||
self.$container.on('input', 'input', $.proxy(function(event) {
|
||||
var $input = $(event.target);
|
||||
//check if the previous inserted value was a space.
|
||||
var text = $input.val();
|
||||
if (text.length > 0) {
|
||||
var lastChar = text.charAt(text.length - 1);
|
||||
if (lastChar == " ") {
|
||||
text = text.replace(" ", "");
|
||||
self.add(text);
|
||||
$input.val('');
|
||||
}
|
||||
}
|
||||
}, self));
|
||||
|
||||
// Remove icon clicked
|
||||
@@ -562,33 +549,4 @@
|
||||
}
|
||||
return (iCaretPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean indicates whether user has pressed an expected key combination.
|
||||
* @param object keyPressEvent: JavaScript event object, refer
|
||||
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
||||
* @param object lookupList: expected key combinations, as in:
|
||||
* [13, {which: 188, shiftKey: true}]
|
||||
*/
|
||||
function keyCombinationInList(keyPressEvent, lookupList) {
|
||||
var found = false;
|
||||
$.each(lookupList, function (index, keyCombination) {
|
||||
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyPressEvent.which === keyCombination.which) {
|
||||
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
|
||||
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
|
||||
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
|
||||
if (alt && shift && ctrl) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
})(window.jQuery);
|
||||
|
||||
Reference in New Issue
Block a user