Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571e87dd14 | ||
|
|
22eb38709a | ||
|
|
986a8420f8 | ||
|
|
cff5f8e8e9 | ||
|
|
0df6cea88c | ||
|
|
88ff2432f6 | ||
|
|
efb7fb999b | ||
|
|
1a2b503791 | ||
|
|
e68d4ffa3a | ||
|
|
d701a8964f | ||
|
|
b6929113bc | ||
|
|
fc1eac1049 | ||
|
|
189659ff1e | ||
|
|
cb19130119 | ||
|
|
1596bf1deb | ||
|
|
636cbb3d21 | ||
|
|
cff7845dd4 | ||
|
|
2d9cc3507f | ||
|
|
4c6ac96dd1 | ||
|
|
e5a594367e | ||
|
|
12aaf8c209 | ||
|
|
cafbb156af | ||
|
|
36ac61d848 | ||
|
|
df066a593e | ||
|
|
7ac9185e4c | ||
|
|
090cbba1a0 | ||
|
|
5e3529f9cc | ||
|
|
f90ddeabb4 | ||
|
|
f54419b993 | ||
|
|
cbbd7ff25f | ||
|
|
0c2c089828 | ||
|
|
6a1813399f | ||
|
|
4c8d2b3704 | ||
|
|
23f97ebd46 | ||
|
|
e3dccf0182 | ||
|
|
272596878c | ||
|
|
46953e9063 | ||
|
|
30f35855f2 | ||
|
|
df94132917 | ||
|
|
de37d689be | ||
|
|
4f941aad52 | ||
|
|
2febdf1259 | ||
|
|
349808a609 | ||
|
|
7d58d1bc40 | ||
|
|
5e91a26230 | ||
|
|
0942ae1742 | ||
|
|
cce9bd29e5 | ||
|
|
05fdeef862 | ||
|
|
dfa08b287c | ||
|
|
77920f94cd | ||
|
|
3631a036c6 | ||
|
|
1445959d05 | ||
|
|
0b45de5ba8 | ||
|
|
d86cdfcefd | ||
|
|
0c7aa304f7 | ||
|
|
e5114f491d | ||
|
|
e238bd8a12 | ||
|
|
f53ad74c72 | ||
|
|
3d456d1cd5 | ||
|
|
de32fe4d85 | ||
|
|
29a90fe814 | ||
|
|
3f31f091dc | ||
|
|
571b558181 | ||
|
|
ed0775ab71 | ||
|
|
19e19d7c15 | ||
|
|
b7e574889c | ||
|
|
2a66c97254 | ||
|
|
e14ef961b9 | ||
|
|
ec2c17be23 | ||
|
|
b5162d2044 | ||
|
|
ae428b94d1 | ||
|
|
3350f3c39a | ||
|
|
68caf82167 | ||
|
|
8c5f1c3a01 | ||
|
|
e1bbc232e1 | ||
|
|
fbc7a39996 | ||
|
|
1ce77ad1ee | ||
|
|
08cee8029f | ||
|
|
e33bcf5e57 | ||
|
|
cf5ceaa959 | ||
|
|
8293bed89c | ||
|
|
a830580c27 | ||
|
|
94c6cca25c | ||
|
|
78e0a0146c | ||
|
|
ea667ed950 | ||
|
|
f8b23b5f20 | ||
|
|
b69749b073 | ||
|
|
951eea844f | ||
|
|
f3e5aff0c9 | ||
|
|
ef3e450e4f | ||
|
|
8272fa20da | ||
|
|
9ef0a748a3 | ||
|
|
6de928aef4 | ||
|
|
34ff874949 | ||
|
|
a653f3ac7f | ||
|
|
dc96084406 | ||
|
|
e056aaa7dc | ||
|
|
73bb3c11b4 | ||
|
|
c88a040728 | ||
|
|
dfa56c2b12 | ||
|
|
104d4bab52 | ||
|
|
6916161e87 | ||
|
|
512852d217 | ||
|
|
a61c699417 | ||
|
|
1e832f014f | ||
|
|
e9a463e2e5 | ||
|
|
d0c19d0436 | ||
|
|
3428abf358 | ||
|
|
e6857331e2 | ||
|
|
1fd93d2efe | ||
|
|
84e44acbfe | ||
|
|
77014c71f2 | ||
|
|
2db01aa4ad | ||
|
|
6c3fa21cc5 | ||
|
|
26b7d101ab | ||
|
|
f3c3cdf0cb | ||
|
|
e62fa31485 | ||
|
|
5f283c1558 | ||
|
|
52163098d2 | ||
|
|
dfe27f0577 | ||
|
|
e415a3468f | ||
|
|
71302a52e4 | ||
|
|
762730eb0f | ||
|
|
3999a8a54d | ||
|
|
065291e146 | ||
|
|
99376aba1c | ||
|
|
698e8a0a95 | ||
|
|
b1b3a127dc | ||
|
|
ec55c95c71 | ||
|
|
e6eb2b473c | ||
|
|
a4b39820e4 | ||
|
|
4a20c81047 | ||
|
|
b72ab461e5 | ||
|
|
0be498d9bd | ||
|
|
00bad986e5 | ||
|
|
4710e84dcf | ||
|
|
ee55f8c884 | ||
|
|
3a74116e70 |
6
.env
6
.env
@@ -7,6 +7,6 @@ MailConfig__Username=""
|
||||
MailConfig__Password=""
|
||||
LOGGING__LOGLEVEL__DEFAULT=Error
|
||||
|
||||
# * Uncoment this line if you use postgresSQL as database backend.
|
||||
# * Check the docker-compose.postgresql.yml file
|
||||
#POSTGRES_CONNECTION="Host=postgres;Username=lubelogger;Password=lubepass;Database=lubelogger;"
|
||||
# This file is provided as a GUIDELINE ONLY
|
||||
# Use the LubeLogger Configurator to configure your environment variables
|
||||
# https://lubelogger.com/configure
|
||||
48
.github/CONTRIBUTING.md
vendored
Normal file
48
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
## Asking Questions
|
||||
Before submitting a question, please check the [Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting) and search through existing issues.
|
||||
|
||||
Ideally, the Issues tab should only consist of bug reports and feature requests instead of debugging your LubeLogger installation.
|
||||
|
||||
## Feature Requests
|
||||
Feature Requests are cool, but we do want to avoid bloat and scope/feature creep.
|
||||
|
||||
LubeLogger is a Vehicle Maintenance and Fuel Mileage Tracker.
|
||||
It is not and should not be used for the following:
|
||||
- Project Management Software(e.g.: Jira)
|
||||
- Budget Management(e.g: YNAB/Actual)
|
||||
- Inventory Management
|
||||
|
||||
Before submitting a feature request, please consider the following:
|
||||
- Will this feature benefit more than just a niche subset of users
|
||||
- Will this feature result in scope creep
|
||||
- Has this feature already been requested/exist
|
||||
|
||||
## Bug Reports
|
||||
Please fill out the issue template for bug reports.
|
||||
|
||||
## Security Vulnerabilities
|
||||
Contact us via [Email](mailto:hargatasoftworks@gmail.com)
|
||||
|
||||
## Submitting Pull Requests
|
||||
Pull Requests are welcome, but please consider the following:
|
||||
|
||||
CI/CD, Bugs, Tech-Debt Related PRs:
|
||||
- Make sure changes are not breaking.
|
||||
- If any dependencies are added, they must be cross-platform compatible.
|
||||
- Please test changes thoroughly.
|
||||
|
||||
Feature Related PRs:
|
||||
- Have you requested this feature in the Issues tab
|
||||
- Will this feature benefit more than just a niche subset of users
|
||||
- Will this feature result in scope creep
|
||||
- Do these changes fix a root cause or is it simply a workaround
|
||||
|
||||
Ideally, you should first submit a feature request in the Issues tab before submitting a PR just in case it's something we are already working on.
|
||||
|
||||
Note the following:
|
||||
|
||||
We(the maintainers) are not responsible for cleaning up, testing, or fixing your changes.
|
||||
|
||||
If your changes doesn't have a wide-ranging use-case, has not been thoroughly tested, or cannot be easily maintained, we won't merge your PR.
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -10,7 +10,7 @@ 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 read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
|
||||
- [ ] I have searched through existing issues.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="MailKit" Version="4.5.0" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.3" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34330.188
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarCareTracker", "CarCareTracker.csproj", "{0DB85611-6555-4127-A66E-DADD25A16526}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarCareTracker", "CarCareTracker.csproj", "{0DB85611-6555-4127-A66E-DADD25A16526}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -99,6 +99,98 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/info")]
|
||||
public IActionResult VehicleInfo(int vehicleId)
|
||||
{
|
||||
//stats for a specific or all vehicles
|
||||
List<Vehicle> vehicles = new List<Vehicle>();
|
||||
if (vehicleId != default)
|
||||
{
|
||||
if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleId))
|
||||
{
|
||||
vehicles.Add(_dataAccess.GetVehicleById(vehicleId));
|
||||
} else
|
||||
{
|
||||
return new RedirectResult("/Error/Unauthorized");
|
||||
}
|
||||
} else
|
||||
{
|
||||
var result = _dataAccess.GetVehicles();
|
||||
if (!User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
result = _userLogic.FilterUserVehicles(result, GetUserID());
|
||||
}
|
||||
vehicles.AddRange(result);
|
||||
}
|
||||
|
||||
List<VehicleInfo> apiResult = new List<VehicleInfo>();
|
||||
|
||||
foreach(Vehicle vehicle in vehicles)
|
||||
{
|
||||
var currentMileage = _vehicleLogic.GetMaxMileage(vehicle.Id);
|
||||
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
|
||||
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
|
||||
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id);
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id);
|
||||
var planRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id);
|
||||
|
||||
var resultToAdd = new VehicleInfo()
|
||||
{
|
||||
VehicleData = vehicle,
|
||||
LastReportedOdometer = currentMileage,
|
||||
ServiceRecordCount = serviceRecords.Count(),
|
||||
ServiceRecordCost = serviceRecords.Sum(x=>x.Cost),
|
||||
RepairRecordCount = repairRecords.Count(),
|
||||
RepairRecordCost = repairRecords.Sum(x=>x.Cost),
|
||||
UpgradeRecordCount = upgradeRecords.Count(),
|
||||
UpgradeRecordCost = upgradeRecords.Sum(x=>x.Cost),
|
||||
GasRecordCount = gasRecords.Count(),
|
||||
GasRecordCost = gasRecords.Sum(x=>x.Cost),
|
||||
TaxRecordCount = taxRecords.Count(),
|
||||
TaxRecordCost = taxRecords.Sum(x=> x.Cost),
|
||||
VeryUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.VeryUrgent),
|
||||
PastDueReminderCount = results.Count(x => x.Urgency == ReminderUrgency.PastDue),
|
||||
UrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.Urgent),
|
||||
NotUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.NotUrgent),
|
||||
PlanRecordBackLogCount = planRecords.Count(x=>x.Progress == PlanProgress.Backlog),
|
||||
PlanRecordInProgressCount = planRecords.Count(x=>x.Progress == PlanProgress.InProgress),
|
||||
PlanRecordTestingCount = planRecords.Count(x=>x.Progress == PlanProgress.Testing),
|
||||
PlanRecordDoneCount = planRecords.Count(x=>x.Progress == PlanProgress.Done)
|
||||
};
|
||||
//set next reminder
|
||||
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date))
|
||||
{
|
||||
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
|
||||
}
|
||||
else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage))
|
||||
{
|
||||
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
|
||||
}
|
||||
apiResult.Add(resultToAdd);
|
||||
}
|
||||
return Json(apiResult);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/adjustedodometer")]
|
||||
public IActionResult AdjustedOdometer(int vehicleId, int odometer)
|
||||
{
|
||||
var vehicle = _dataAccess.GetVehicleById(vehicleId);
|
||||
if (vehicle == null || !vehicle.HasOdometerAdjustment)
|
||||
{
|
||||
return Json(odometer);
|
||||
} else
|
||||
{
|
||||
var convertedOdometer = (odometer + int.Parse(vehicle.OdometerDifference)) * decimal.Parse(vehicle.OdometerMultiplier);
|
||||
return Json(convertedOdometer);
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/servicerecords")]
|
||||
@@ -594,6 +686,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var vehicles = _dataAccess.GetVehicles();
|
||||
List<OperationResponse> operationResponses = new List<OperationResponse>();
|
||||
var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail;
|
||||
foreach(Vehicle vehicle in vehicles)
|
||||
{
|
||||
var vehicleId = vehicle.Id;
|
||||
@@ -609,6 +702,10 @@ namespace CarCareTracker.Controllers
|
||||
//get list of recipients.
|
||||
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
|
||||
List<string> emailRecipients = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(defaultEmailAddress))
|
||||
{
|
||||
emailRecipients.Add(defaultEmailAddress);
|
||||
}
|
||||
foreach (int userId in userIds)
|
||||
{
|
||||
var userData = _userRecordDataAccess.GetUserRecordById(userId);
|
||||
@@ -621,15 +718,19 @@ namespace CarCareTracker.Controllers
|
||||
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
|
||||
operationResponses.Add(result);
|
||||
}
|
||||
if (operationResponses.All(x => x.Success))
|
||||
if (!operationResponses.Any())
|
||||
{
|
||||
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
|
||||
return Json(new OperationResponse { Success = false, Message = "No Emails Sent, No Vehicles Available or No Recipients Configured" });
|
||||
}
|
||||
else if (operationResponses.All(x => x.Success))
|
||||
{
|
||||
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count()})" });
|
||||
} else if (operationResponses.All(x => !x.Success))
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
|
||||
return Json(new OperationResponse { Success = false, Message = $"All Emails Failed({operationResponses.Count()}), Check SMTP Settings" });
|
||||
} else
|
||||
{
|
||||
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
|
||||
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count(x => x.Success)}), Emails Failed({operationResponses.Count(x => !x.Success)}), Check Recipient Settings" });
|
||||
}
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
|
||||
@@ -22,15 +22,46 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var viewModel = new AdminViewModel
|
||||
{
|
||||
Users = _loginLogic.GetAllUsers(),
|
||||
Users = _loginLogic.GetAllUsers().OrderBy(x=>x.Id).ToList(),
|
||||
Tokens = _loginLogic.GetAllTokens()
|
||||
};
|
||||
return View(viewModel);
|
||||
}
|
||||
public IActionResult GetTokenPartialView()
|
||||
{
|
||||
var viewModel = _loginLogic.GetAllTokens();
|
||||
return PartialView("_Tokens", viewModel);
|
||||
}
|
||||
public IActionResult GetUserPartialView()
|
||||
{
|
||||
var viewModel = _loginLogic.GetAllUsers().OrderBy(x => x.Id).ToList();
|
||||
return PartialView("_Users", viewModel);
|
||||
}
|
||||
public IActionResult GenerateNewToken(string emailAddress, bool autoNotify)
|
||||
{
|
||||
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
|
||||
return Json(result);
|
||||
if (emailAddress.Contains(","))
|
||||
{
|
||||
string[] emailAddresses = emailAddress.Split(',');
|
||||
foreach(string emailAdd in emailAddresses)
|
||||
{
|
||||
var trimmedEmail = emailAdd.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(trimmedEmail))
|
||||
{
|
||||
var result = _loginLogic.GenerateUserToken(emailAdd.Trim(), autoNotify);
|
||||
if (!result.Success)
|
||||
{
|
||||
//if fail, return prematurely
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
var successResponse = new OperationResponse { Success = true, Message = "Token Generated!" };
|
||||
return Json(successResponse);
|
||||
} else
|
||||
{
|
||||
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteToken(int tokenId)
|
||||
|
||||
@@ -92,5 +92,18 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return Path.Combine("/", uploadDirectory, fileName);
|
||||
}
|
||||
public IActionResult UploadCoordinates(List<string> coordinates)
|
||||
{
|
||||
string uploadDirectory = "temp/";
|
||||
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
|
||||
if (!Directory.Exists(uploadPath))
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
string fileName = Guid.NewGuid() + ".csv";
|
||||
string filePath = Path.Combine(uploadPath, fileName);
|
||||
string fileData = string.Join("\r\n", coordinates);
|
||||
System.IO.File.WriteAllText(filePath, fileData);
|
||||
var uploadedFile = new UploadedFiles { Name = "coordinates.csv", Location = Path.Combine("/", uploadDirectory, fileName) };
|
||||
return Json(uploadedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace CarCareTracker.Controllers
|
||||
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
private readonly ITranslationHelper _translationHelper;
|
||||
public HomeController(ILogger<HomeController> logger,
|
||||
IVehicleDataAccess dataAccess,
|
||||
IUserLogic userLogic,
|
||||
@@ -31,7 +32,8 @@ namespace CarCareTracker.Controllers
|
||||
IFileHelper fileHelper,
|
||||
IExtraFieldDataAccess extraFieldDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IReminderHelper reminderHelper)
|
||||
IReminderHelper reminderHelper,
|
||||
ITranslationHelper translationHelper)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataAccess = dataAccess;
|
||||
@@ -43,6 +45,7 @@ namespace CarCareTracker.Controllers
|
||||
_reminderHelper = reminderHelper;
|
||||
_loginLogic = loginLogic;
|
||||
_vehicleLogic = vehicleLogic;
|
||||
_translationHelper = translationHelper;
|
||||
}
|
||||
private int GetUserID()
|
||||
{
|
||||
@@ -59,22 +62,52 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
|
||||
}
|
||||
var vehicleViewModels = vehiclesStored.Select(x => new VehicleViewModel
|
||||
var vehicleViewModels = vehiclesStored.Select(x =>
|
||||
{
|
||||
Id = x.Id,
|
||||
ImageLocation = x.ImageLocation,
|
||||
Year = x.Year,
|
||||
Make = x.Make,
|
||||
Model = x.Model,
|
||||
LicensePlate = x.LicensePlate,
|
||||
SoldDate = x.SoldDate,
|
||||
IsElectric = x.IsElectric,
|
||||
IsDiesel = x.IsDiesel,
|
||||
UseHours = x.UseHours,
|
||||
ExtraFields = x.ExtraFields,
|
||||
Tags = x.Tags,
|
||||
LastReportedMileage = _vehicleLogic.GetMaxMileage(x.Id),
|
||||
HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id)
|
||||
var vehicleVM = new VehicleViewModel
|
||||
{
|
||||
Id = x.Id,
|
||||
ImageLocation = x.ImageLocation,
|
||||
Year = x.Year,
|
||||
Make = x.Make,
|
||||
Model = x.Model,
|
||||
LicensePlate = x.LicensePlate,
|
||||
SoldDate = x.SoldDate,
|
||||
IsElectric = x.IsElectric,
|
||||
IsDiesel = x.IsDiesel,
|
||||
UseHours = x.UseHours,
|
||||
OdometerOptional = x.OdometerOptional,
|
||||
ExtraFields = x.ExtraFields,
|
||||
Tags = x.Tags,
|
||||
DashboardMetrics = x.DashboardMetrics,
|
||||
VehicleIdentifier = x.VehicleIdentifier
|
||||
};
|
||||
//dashboard metrics
|
||||
if (x.DashboardMetrics.Any())
|
||||
{
|
||||
var vehicleRecords = _vehicleLogic.GetVehicleRecords(x.Id);
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
var distanceUnit = x.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km";
|
||||
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.Default))
|
||||
{
|
||||
vehicleVM.LastReportedMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
|
||||
vehicleVM.HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id, vehicleVM.LastReportedMileage);
|
||||
}
|
||||
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.CostPerMile))
|
||||
{
|
||||
var vehicleTotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
|
||||
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
|
||||
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
|
||||
var totalDistance = maxMileage - minMileage;
|
||||
vehicleVM.CostPerMile = totalDistance != default ? vehicleTotalCost / totalDistance : 0.00M;
|
||||
vehicleVM.DistanceUnit = distanceUnit;
|
||||
}
|
||||
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.TotalCost))
|
||||
{
|
||||
vehicleVM.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
|
||||
}
|
||||
}
|
||||
return vehicleVM;
|
||||
}).ToList();
|
||||
return PartialView("_GarageDisplay", vehicleViewModels);
|
||||
}
|
||||
@@ -94,7 +127,7 @@ namespace CarCareTracker.Controllers
|
||||
if (vehicleReminders.Any())
|
||||
{
|
||||
var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, 0, DateTime.Now);
|
||||
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();
|
||||
reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Date = x.Date, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" }).ToList();
|
||||
reminders.AddRange(reminderUrgency);
|
||||
}
|
||||
}
|
||||
@@ -115,17 +148,21 @@ namespace CarCareTracker.Controllers
|
||||
UserConfig = userConfig,
|
||||
UILanguages = languages
|
||||
};
|
||||
return PartialView("_Settings", viewModel);
|
||||
}
|
||||
public async Task<IActionResult> Sponsors()
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var sponsorsData = await httpClient.GetFromJsonAsync<Sponsors>(StaticHelper.SponsorsPath) ?? new Sponsors();
|
||||
viewModel.Sponsors = sponsorsData;
|
||||
return PartialView("_Sponsors", sponsorsData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Unable to retrieve sponsors: {ex.Message}");
|
||||
return PartialView("_Sponsors", new Sponsors());
|
||||
}
|
||||
return PartialView("_Settings", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult WriteToSettings(UserConfig userConfig)
|
||||
@@ -188,7 +225,8 @@ namespace CarCareTracker.Controllers
|
||||
return Json(result);
|
||||
}
|
||||
return Json(false);
|
||||
} catch (Exception ex)
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return Json(false);
|
||||
@@ -205,7 +243,7 @@ namespace CarCareTracker.Controllers
|
||||
var result = _loginLogic.UpdateUserDetails(userId, userAccount);
|
||||
return Json(result);
|
||||
}
|
||||
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage});
|
||||
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -227,6 +265,209 @@ namespace CarCareTracker.Controllers
|
||||
var userName = User.Identity.Name;
|
||||
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
public IActionResult GetTranslatorEditor(string userLanguage)
|
||||
{
|
||||
var translationData = _translationHelper.GetTranslations(userLanguage);
|
||||
return PartialView("_TranslationEditor", translationData);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpPost]
|
||||
public IActionResult SaveTranslation(string userLanguage, Dictionary<string, string> translationData)
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(userLanguage, translationData);
|
||||
return Json(result);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpPost]
|
||||
public IActionResult ExportTranslation(Dictionary<string, string> translationData)
|
||||
{
|
||||
var result = _translationHelper.ExportTranslation(translationData);
|
||||
return Json(result);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAvailableTranslations()
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
|
||||
return PartialView("_Translations", translations);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
|
||||
return PartialView("_Translations", new Translations());
|
||||
}
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> DownloadTranslation(string continent, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath(continent, name)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(name, translationData);
|
||||
if (!result.Success)
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError($"Unable to download translation: {name}");
|
||||
return Json(false);
|
||||
}
|
||||
return Json(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Unable to download translation: {ex.Message}");
|
||||
return Json(false);
|
||||
}
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> DownloadAllTranslations()
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var translations = await httpClient.GetFromJsonAsync<Translations>(StaticHelper.TranslationDirectoryPath) ?? new Translations();
|
||||
int translationsDownloaded = 0;
|
||||
foreach (string translation in translations.Asia)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Asia", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
foreach (string translation in translations.Africa)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Africa", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
foreach (string translation in translations.Europe)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Europe", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
foreach (string translation in translations.NorthAmerica)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("NorthAmerica", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
foreach (string translation in translations.SouthAmerica)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("SouthAmerica", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
foreach (string translation in translations.Oceania)
|
||||
{
|
||||
try
|
||||
{
|
||||
var translationData = await httpClient.GetFromJsonAsync<Dictionary<string, string>>(StaticHelper.GetTranslationDownloadPath("Oceania", translation)) ?? new Dictionary<string, string>();
|
||||
if (translationData.Any())
|
||||
{
|
||||
var result = _translationHelper.SaveTranslation(translation, translationData);
|
||||
if (result.Success)
|
||||
{
|
||||
translationsDownloaded++;
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Error Downloading Translation {translation}: {ex.Message} ");
|
||||
}
|
||||
}
|
||||
if (translationsDownloaded > 0)
|
||||
{
|
||||
return Json(new OperationResponse() { Success = true, Message = $"{translationsDownloaded} Translations Downloaded" });
|
||||
} else
|
||||
{
|
||||
return Json(new OperationResponse() { Success = false, Message = "No Translations Downloaded" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Unable to retrieve translations: {ex.Message}");
|
||||
return Json(new OperationResponse() { Success = false, Message = StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
}
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
|
||||
@@ -51,6 +51,10 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
public IActionResult Registration()
|
||||
{
|
||||
if (_config.GetServerDisabledRegistration())
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
return View();
|
||||
}
|
||||
public IActionResult ForgotPassword()
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var cmds = new List<string>
|
||||
{
|
||||
"CREATE SCHEMA IF NOT EXISTS app",
|
||||
"CREATE TABLE IF NOT EXISTS app.vehicles (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)",
|
||||
"CREATE TABLE IF NOT EXISTS app.collisionrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
|
||||
"CREATE TABLE IF NOT EXISTS app.upgraderecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
|
||||
|
||||
187
Controllers/Vehicle/GasController.cs
Normal file
187
Controllers/Vehicle/GasController.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
//check if the user uses MPG or Liters per 100km.
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
bool useMPG = userConfig.UseMPG;
|
||||
bool useUKMPG = userConfig.UseUKMPG;
|
||||
var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG);
|
||||
if (userConfig.UseDescending)
|
||||
{
|
||||
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var vehicleIsElectric = vehicleData.IsElectric;
|
||||
var vehicleUseHours = vehicleData.UseHours;
|
||||
var viewModel = new GasRecordViewModelContainer()
|
||||
{
|
||||
UseKwh = vehicleIsElectric,
|
||||
UseHours = vehicleUseHours,
|
||||
GasRecords = computedResults
|
||||
};
|
||||
return PartialView("_Gas", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord)
|
||||
{
|
||||
if (gasRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
|
||||
{
|
||||
Date = DateTime.Parse(gasRecord.Date),
|
||||
VehicleId = gasRecord.VehicleId,
|
||||
Mileage = gasRecord.Mileage,
|
||||
Notes = $"Auto Insert From Gas Record. {gasRecord.Notes}"
|
||||
});
|
||||
}
|
||||
gasRecord.Files = gasRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord.ToGasRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), gasRecord.VehicleId, User.Identity.Name, $"{(gasRecord.Id == default ? "Created" : "Edited")} Gas Record - Mileage: {gasRecord.Mileage.ToString()}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddGasRecordPartialView(int vehicleId)
|
||||
{
|
||||
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var vehicleIsElectric = vehicleData.IsElectric;
|
||||
var vehicleUseHours = vehicleData.UseHours;
|
||||
return PartialView("_GasModal", new GasRecordInputContainer() { UseKwh = vehicleIsElectric, UseHours = vehicleUseHours, GasRecord = new GasRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields } });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetGasRecordForEditById(int gasRecordId)
|
||||
{
|
||||
var result = _gasRecordDataAccess.GetGasRecordById(gasRecordId);
|
||||
var convertedResult = new GasRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Mileage = result.Mileage,
|
||||
VehicleId = result.VehicleId,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Files = result.Files,
|
||||
Gallons = result.Gallons,
|
||||
IsFillToFull = result.IsFillToFull,
|
||||
MissedFuelUp = result.MissedFuelUp,
|
||||
Notes = result.Notes,
|
||||
Tags = result.Tags,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields)
|
||||
};
|
||||
var vehicleData = _dataAccess.GetVehicleById(convertedResult.VehicleId);
|
||||
var vehicleIsElectric = vehicleData.IsElectric;
|
||||
var vehicleUseHours = vehicleData.UseHours;
|
||||
var viewModel = new GasRecordInputContainer()
|
||||
{
|
||||
UseKwh = vehicleIsElectric,
|
||||
UseHours = vehicleUseHours,
|
||||
GasRecord = convertedResult
|
||||
};
|
||||
return PartialView("_GasModal", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteGasRecordById(int gasRecordId)
|
||||
{
|
||||
var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Gas Record - Id: {gasRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit)
|
||||
{
|
||||
var currentConfig = _config.GetUserConfig(User);
|
||||
currentConfig.PreferredGasUnit = gasUnit;
|
||||
currentConfig.PreferredGasMileageUnit = fuelMileageUnit;
|
||||
var result = _config.SaveUserConfig(User, currentConfig);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetGasRecordsEditModal(List<int> recordIds)
|
||||
{
|
||||
var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields;
|
||||
return PartialView("_GasRecordsModal", new GasRecordEditModel { RecordIds = recordIds, EditRecord = new GasRecord { ExtraFields = extraFields } });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveMultipleGasRecords(GasRecordEditModel editModel)
|
||||
{
|
||||
var dateIsEdited = editModel.EditRecord.Date != default;
|
||||
var mileageIsEdited = editModel.EditRecord.Mileage != default;
|
||||
var consumptionIsEdited = editModel.EditRecord.Gallons != default;
|
||||
var costIsEdited = editModel.EditRecord.Cost != default;
|
||||
var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes);
|
||||
var tagsIsEdited = editModel.EditRecord.Tags.Any();
|
||||
var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any();
|
||||
//handle clear overrides
|
||||
if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---"))
|
||||
{
|
||||
editModel.EditRecord.Tags = new List<string>();
|
||||
}
|
||||
if (noteIsEdited && editModel.EditRecord.Notes == "---")
|
||||
{
|
||||
editModel.EditRecord.Notes = "";
|
||||
}
|
||||
bool result = false;
|
||||
foreach (int recordId in editModel.RecordIds)
|
||||
{
|
||||
var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId);
|
||||
if (dateIsEdited)
|
||||
{
|
||||
existingRecord.Date = editModel.EditRecord.Date;
|
||||
}
|
||||
if (consumptionIsEdited)
|
||||
{
|
||||
existingRecord.Gallons = editModel.EditRecord.Gallons;
|
||||
}
|
||||
if (costIsEdited)
|
||||
{
|
||||
existingRecord.Cost = editModel.EditRecord.Cost;
|
||||
}
|
||||
if (mileageIsEdited)
|
||||
{
|
||||
existingRecord.Mileage = editModel.EditRecord.Mileage;
|
||||
}
|
||||
if (noteIsEdited)
|
||||
{
|
||||
existingRecord.Notes = editModel.EditRecord.Notes;
|
||||
}
|
||||
if (tagsIsEdited)
|
||||
{
|
||||
existingRecord.Tags = editModel.EditRecord.Tags;
|
||||
}
|
||||
if (extraFieldIsEdited)
|
||||
{
|
||||
foreach (ExtraField extraField in editModel.EditRecord.ExtraFields)
|
||||
{
|
||||
if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name))
|
||||
{
|
||||
var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name);
|
||||
existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name);
|
||||
existingRecord.ExtraFields.Insert(insertIndex, extraField);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingRecord.ExtraFields.Add(extraField);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord);
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
485
Controllers/Vehicle/ImportController.cs
Normal file
485
Controllers/Vehicle/ImportController.cs
Normal file
@@ -0,0 +1,485 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.MapProfile;
|
||||
using CarCareTracker.Models;
|
||||
using CsvHelper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
|
||||
{
|
||||
return PartialView("_BulkDataImporter", mode);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
|
||||
{
|
||||
if (vehicleId == default && mode != ImportMode.SupplyRecord)
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
string uploadDirectory = "temp/";
|
||||
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
|
||||
if (!Directory.Exists(uploadPath))
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
if (mode == ImportMode.ServiceRecord)
|
||||
{
|
||||
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Notes = x.Notes,
|
||||
Odometer = x.Mileage.ToString(),
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
//custom writer
|
||||
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
|
||||
}
|
||||
writer.Dispose();
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.RepairRecord)
|
||||
{
|
||||
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Notes = x.Notes,
|
||||
Odometer = x.Mileage.ToString(),
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new GenericRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Notes = x.Notes,
|
||||
Odometer = x.Mileage.ToString(),
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteGenericRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.OdometerRecord)
|
||||
{
|
||||
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Notes = x.Notes,
|
||||
InitialOdometer = x.InitialMileage.ToString(),
|
||||
Odometer = x.Mileage.ToString(),
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteOdometerRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.SupplyRecord)
|
||||
{
|
||||
var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new SupplyRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
PartNumber = x.PartNumber,
|
||||
PartQuantity = x.Quantity.ToString(),
|
||||
PartSupplier = x.PartSupplier,
|
||||
Notes = x.Notes,
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteSupplyRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord)
|
||||
{
|
||||
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new TaxRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Notes = x.Notes,
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteTaxRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.PlanRecord)
|
||||
{
|
||||
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new PlanRecordExportModel
|
||||
{
|
||||
DateCreated = x.DateCreated.ToString("G"),
|
||||
DateModified = x.DateModified.ToString("G"),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Type = x.ImportMode.ToString(),
|
||||
Priority = x.Priority.ToString(),
|
||||
Progress = x.Progress.ToString(),
|
||||
Notes = x.Notes,
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WritePlanRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.GasRecord)
|
||||
{
|
||||
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
||||
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
||||
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
|
||||
var exportData = convertedRecords.Select(x => new GasRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToString(),
|
||||
Cost = x.Cost.ToString(),
|
||||
FuelConsumed = x.Gallons.ToString(),
|
||||
FuelEconomy = x.MilesPerGallon.ToString(),
|
||||
Odometer = x.Mileage.ToString(),
|
||||
IsFillToFull = x.IsFillToFull.ToString(),
|
||||
MissedFuelUp = x.MissedFuelUp.ToString(),
|
||||
Notes = x.Notes,
|
||||
Tags = string.Join(" ", x.Tags),
|
||||
ExtraFields = x.ExtraFields
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
StaticHelper.WriteGasRecordExportModel(csv, exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
return Json(false);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
|
||||
{
|
||||
if (vehicleId == default && mode != ImportMode.SupplyRecord)
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
var fullFileName = _fileHelper.GetFullFilePath(fileName);
|
||||
if (string.IsNullOrWhiteSpace(fullFileName))
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
try
|
||||
{
|
||||
using (var reader = new StreamReader(fullFileName))
|
||||
{
|
||||
var config = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture);
|
||||
config.MissingFieldFound = null;
|
||||
config.HeaderValidated = null;
|
||||
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
|
||||
using (var csv = new CsvReader(reader, config))
|
||||
{
|
||||
csv.Context.RegisterClassMap<ImportMapper>();
|
||||
var records = csv.GetRecords<ImportModel>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
var requiredExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)mode).ExtraFields.Where(x => x.IsRequired).Select(y => y.Name);
|
||||
foreach (ImportModel importModel in records)
|
||||
{
|
||||
if (mode == ImportMode.GasRecord)
|
||||
{
|
||||
//convert to gas model.
|
||||
var convertedRecord = new GasRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
|
||||
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
|
||||
{
|
||||
//cost was not given but price is.
|
||||
//fuelly sometimes exports CSVs without total cost.
|
||||
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
|
||||
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp))
|
||||
{
|
||||
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
|
||||
convertedRecord.IsFillToFull = !parsedBool;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
|
||||
{
|
||||
var possibleFillToFullValues = new List<string> { "1", "true", "full" };
|
||||
var parsedBool = possibleFillToFullValues.Contains(importModel.IsFillToFull.Trim().ToLower());
|
||||
convertedRecord.IsFillToFull = parsedBool;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(importModel.MissedFuelUp))
|
||||
{
|
||||
var possibleMissedFuelUpValues = new List<string> { "1", "true" };
|
||||
var parsedBool = possibleMissedFuelUpValues.Contains(importModel.MissedFuelUp.Trim().ToLower());
|
||||
convertedRecord.MissedFuelUp = parsedBool;
|
||||
}
|
||||
//insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error.
|
||||
if (convertedRecord.Gallons > 0)
|
||||
{
|
||||
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(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)
|
||||
{
|
||||
var convertedRecord = new ServiceRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(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)
|
||||
{
|
||||
var convertedRecord = new OdometerRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
InitialMileage = string.IsNullOrWhiteSpace(importModel.InitialOdometer) ? 0 : decimal.ToInt32(decimal.Parse(importModel.InitialOdometer, NumberStyles.Any)),
|
||||
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.PlanRecord)
|
||||
{
|
||||
var progressIsEnum = Enum.TryParse(importModel.Progress, out PlanProgress parsedProgress);
|
||||
var typeIsEnum = Enum.TryParse(importModel.Type, out ImportMode parsedType);
|
||||
var priorityIsEnum = Enum.TryParse(importModel.Priority, out PlanPriority parsedPriority);
|
||||
var convertedRecord = new PlanRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
DateCreated = DateTime.Parse(importModel.DateCreated),
|
||||
DateModified = DateTime.Parse(importModel.DateModified),
|
||||
Progress = parsedProgress,
|
||||
ImportMode = parsedType,
|
||||
Priority = parsedPriority,
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.RepairRecord)
|
||||
{
|
||||
var convertedRecord = new CollisionRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(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)
|
||||
{
|
||||
var convertedRecord = new UpgradeRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(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)
|
||||
{
|
||||
var convertedRecord = new SupplyRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
PartNumber = importModel.PartNumber,
|
||||
PartSupplier = importModel.PartSupplier,
|
||||
Quantity = decimal.Parse(importModel.PartQuantity, NumberStyles.Any),
|
||||
Description = importModel.Description,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Notes = importModel.Notes,
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord)
|
||||
{
|
||||
var convertedRecord = new TaxRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(),
|
||||
ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List<ExtraField>()
|
||||
};
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Json(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error Occurred While Bulk Inserting");
|
||||
return Json(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Controllers/Vehicle/NoteController.cs
Normal file
78
Controllers/Vehicle/NoteController.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
|
||||
result = result.OrderByDescending(x => x.Pinned).ToList();
|
||||
return PartialView("_Notes", result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetPinnedNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
|
||||
result = result.Where(x => x.Pinned).ToList();
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveNoteToVehicleId(Note note)
|
||||
{
|
||||
note.Files = note.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _noteDataAccess.SaveNoteToVehicle(note);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), note.VehicleId, User.Identity.Name, $"{(note.Id == default ? "Created" : "Edited")} Note - Description: {note.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddNotePartialView()
|
||||
{
|
||||
return PartialView("_NoteModal", new Note());
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetNoteForEditById(int noteId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNoteById(noteId);
|
||||
return PartialView("_NoteModal", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteNoteById(int noteId)
|
||||
{
|
||||
var result = _noteDataAccess.DeleteNoteById(noteId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Note - Id: {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
Controllers/Vehicle/OdometerController.cs
Normal file
154
Controllers/Vehicle/OdometerController.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult ForceRecalculateDistanceByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
result = _odometerLogic.AutoConvertOdometerRecord(result);
|
||||
return Json(result.Any());
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetOdometerRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
//determine if conversion is needed.
|
||||
if (result.All(x => x.InitialMileage == default))
|
||||
{
|
||||
result = _odometerLogic.AutoConvertOdometerRecord(result);
|
||||
}
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_OdometerRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveOdometerRecordToVehicleId(OdometerRecordInput odometerRecord)
|
||||
{
|
||||
//move files from temp.
|
||||
odometerRecord.Files = odometerRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord.ToOdometerRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), odometerRecord.VehicleId, User.Identity.Name, $"{(odometerRecord.Id == default ? "Created" : "Edited")} Odometer Record - Mileage: {odometerRecord.Mileage.ToString()}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddOdometerRecordPartialView(int vehicleId)
|
||||
{
|
||||
return PartialView("_OdometerRecordModal", new OdometerRecordInput() { InitialMileage = _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List<OdometerRecord>()), ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetOdometerRecordsEditModal(List<int> recordIds)
|
||||
{
|
||||
var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields;
|
||||
return PartialView("_OdometerRecordsModal", new OdometerRecordEditModel { RecordIds = recordIds, EditRecord = new OdometerRecord { ExtraFields = extraFields } });
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveMultipleOdometerRecords(OdometerRecordEditModel editModel)
|
||||
{
|
||||
var dateIsEdited = editModel.EditRecord.Date != default;
|
||||
var initialMileageIsEdited = editModel.EditRecord.InitialMileage != default;
|
||||
var mileageIsEdited = editModel.EditRecord.Mileage != default;
|
||||
var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes);
|
||||
var tagsIsEdited = editModel.EditRecord.Tags.Any();
|
||||
var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any();
|
||||
//handle clear overrides
|
||||
if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---"))
|
||||
{
|
||||
editModel.EditRecord.Tags = new List<string>();
|
||||
}
|
||||
if (noteIsEdited && editModel.EditRecord.Notes == "---")
|
||||
{
|
||||
editModel.EditRecord.Notes = "";
|
||||
}
|
||||
bool result = false;
|
||||
foreach (int recordId in editModel.RecordIds)
|
||||
{
|
||||
var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId);
|
||||
if (dateIsEdited)
|
||||
{
|
||||
existingRecord.Date = editModel.EditRecord.Date;
|
||||
}
|
||||
if (initialMileageIsEdited)
|
||||
{
|
||||
existingRecord.InitialMileage = editModel.EditRecord.InitialMileage;
|
||||
}
|
||||
if (mileageIsEdited)
|
||||
{
|
||||
existingRecord.Mileage = editModel.EditRecord.Mileage;
|
||||
}
|
||||
if (noteIsEdited)
|
||||
{
|
||||
existingRecord.Notes = editModel.EditRecord.Notes;
|
||||
}
|
||||
if (tagsIsEdited)
|
||||
{
|
||||
existingRecord.Tags = editModel.EditRecord.Tags;
|
||||
}
|
||||
if (extraFieldIsEdited)
|
||||
{
|
||||
foreach (ExtraField extraField in editModel.EditRecord.ExtraFields)
|
||||
{
|
||||
if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name))
|
||||
{
|
||||
var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name);
|
||||
existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name);
|
||||
existingRecord.ExtraFields.Insert(insertIndex, extraField);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingRecord.ExtraFields.Add(extraField);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord);
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetOdometerRecordForEditById(int odometerRecordId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new OdometerRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
InitialMileage = result.InitialMileage,
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_OdometerRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteOdometerRecordById(int odometerRecordId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.DeleteOdometerRecordById(odometerRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Odometer Record - Id: {odometerRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
243
Controllers/Vehicle/PlanController.cs
Normal file
243
Controllers/Vehicle/PlanController.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
|
||||
return PartialView("_PlanRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SavePlanRecordToVehicleId(PlanRecordInput planRecord)
|
||||
{
|
||||
//populate createdDate
|
||||
if (planRecord.Id == default)
|
||||
{
|
||||
planRecord.DateCreated = DateTime.Now.ToString("G");
|
||||
}
|
||||
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();
|
||||
if (planRecord.Supplies.Any())
|
||||
{
|
||||
planRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description);
|
||||
if (planRecord.CopySuppliesAttachment)
|
||||
{
|
||||
planRecord.Files.AddRange(GetSuppliesAttachments(planRecord.Supplies));
|
||||
}
|
||||
}
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), planRecord.VehicleId, User.Identity.Name, $"{(planRecord.Id == default ? "Created" : "Edited")} Plan Record - Description: {planRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord)
|
||||
{
|
||||
//check if template name already taken.
|
||||
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any();
|
||||
if (planRecord.Id == default && existingRecord)
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "A template with that description already exists for this vehicle" });
|
||||
}
|
||||
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord);
|
||||
return Json(new OperationResponse { Success = result, Message = result ? "Template Added" : StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordTemplatesForVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicleId);
|
||||
return PartialView("_PlanRecordTemplateModal", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeletePlanRecordTemplateById(int planRecordTemplateId)
|
||||
{
|
||||
var result = _planRecordTemplateDataAccess.DeletePlanRecordTemplateById(planRecordTemplateId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult ConvertPlanRecordTemplateToPlanRecord(int planRecordTemplateId)
|
||||
{
|
||||
var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
|
||||
if (existingRecord.Id == default)
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "Unable to find template" });
|
||||
}
|
||||
if (existingRecord.Supplies.Any())
|
||||
{
|
||||
//check if all supplies are available
|
||||
var supplyAvailability = CheckSupplyRecordsAvailability(existingRecord.Supplies);
|
||||
if (supplyAvailability.Any())
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = string.Join("<br>", supplyAvailability) });
|
||||
}
|
||||
}
|
||||
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;
|
||||
if (existingRecord.Supplies.Any())
|
||||
{
|
||||
existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description);
|
||||
if (existingRecord.CopySuppliesAttachment)
|
||||
{
|
||||
existingRecord.Files.AddRange(GetSuppliesAttachments(existingRecord.Supplies));
|
||||
}
|
||||
}
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord());
|
||||
return Json(new OperationResponse { Success = result, Message = result ? "Plan Record Added" : StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddPlanRecordPartialView()
|
||||
{
|
||||
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);
|
||||
existingRecord.Progress = planProgress;
|
||||
existingRecord.DateModified = DateTime.Now;
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
|
||||
if (planProgress == PlanProgress.Done)
|
||||
{
|
||||
if (_config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
|
||||
{
|
||||
Date = DateTime.Now.Date,
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Mileage = odometer,
|
||||
Notes = $"Auto Insert From Plan Record: {existingRecord.Description}",
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
});
|
||||
}
|
||||
//convert plan record to service/upgrade/repair record.
|
||||
if (existingRecord.ImportMode == ImportMode.ServiceRecord)
|
||||
{
|
||||
var newRecord = new ServiceRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now.Date,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files,
|
||||
RequisitionHistory = existingRecord.RequisitionHistory,
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
|
||||
}
|
||||
else if (existingRecord.ImportMode == ImportMode.RepairRecord)
|
||||
{
|
||||
var newRecord = new CollisionRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now.Date,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files,
|
||||
RequisitionHistory = existingRecord.RequisitionHistory,
|
||||
ExtraFields = existingRecord.ExtraFields
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
|
||||
}
|
||||
else if (existingRecord.ImportMode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
var newRecord = new UpgradeRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now.Date,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
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, DateTime.Now, odometer);
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordTemplateForEditById(int planRecordTemplateId)
|
||||
{
|
||||
var result = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
|
||||
return PartialView("_PlanRecordTemplateEditModal", result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordForEditById(int planRecordId)
|
||||
{
|
||||
var result = _planRecordDataAccess.GetPlanRecordById(planRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new PlanRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Description = result.Description,
|
||||
DateCreated = result.DateCreated.ToString("G"),
|
||||
DateModified = result.DateModified.ToString("G"),
|
||||
ImportMode = result.ImportMode,
|
||||
Priority = result.Priority,
|
||||
Progress = result.Progress,
|
||||
Cost = result.Cost,
|
||||
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);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeletePlanRecordById(int planRecordId)
|
||||
{
|
||||
var result = _planRecordDataAccess.DeletePlanRecordById(planRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Plan Record - Id: {planRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
Controllers/Vehicle/ReminderController.cs
Normal file
163
Controllers/Vehicle/ReminderController.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
|
||||
{
|
||||
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
|
||||
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
|
||||
return results;
|
||||
}
|
||||
private bool GetAndUpdateVehicleUrgentOrPastDueReminders(int vehicleId)
|
||||
{
|
||||
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
//check if user wants auto-refresh past-due reminders
|
||||
if (_config.GetUserConfig(User).EnableAutoReminderRefresh)
|
||||
{
|
||||
//check for past due reminders that are eligible for recurring.
|
||||
var pastDueAndRecurring = result.Where(x => x.Urgency == ReminderUrgency.PastDue && x.IsRecurring);
|
||||
if (pastDueAndRecurring.Any())
|
||||
{
|
||||
foreach (ReminderRecordViewModel reminderRecord in pastDueAndRecurring)
|
||||
{
|
||||
//update based on recurring intervals.
|
||||
//pull reminderRecord based on ID
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecord.Id);
|
||||
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, null, null);
|
||||
//save to db.
|
||||
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
|
||||
//set urgency to not urgent so it gets excluded in count.
|
||||
reminderRecord.Urgency = ReminderUrgency.NotUrgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
//check for very urgent or past due reminders that were not eligible for recurring.
|
||||
var pastDueAndUrgentReminders = result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
|
||||
if (pastDueAndUrgentReminders.Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
|
||||
{
|
||||
var result = GetAndUpdateVehicleUrgentOrPastDueReminders(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
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 result = PushbackRecurringReminderRecordWithChecks(reminderRecordId, null, null);
|
||||
return Json(result);
|
||||
}
|
||||
private bool PushbackRecurringReminderRecordWithChecks(int reminderRecordId, DateTime? currentDate, int? currentMileage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
|
||||
if (existingReminder is not null && existingReminder.Id != default && existingReminder.IsRecurring)
|
||||
{
|
||||
existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, currentDate, currentMileage);
|
||||
//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)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord.ToReminderRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), reminderRecord.VehicleId, User.Identity.Name, $"{(reminderRecord.Id == default ? "Created" : "Edited")} Reminder - Description: {reminderRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetAddReminderRecordPartialView(ReminderRecordInput? reminderModel)
|
||||
{
|
||||
if (reminderModel is not null)
|
||||
{
|
||||
return PartialView("_ReminderRecordModal", reminderModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return PartialView("_ReminderRecordModal", new ReminderRecordInput());
|
||||
}
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetReminderRecordForEditById(int reminderRecordId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new ReminderRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Mileage = result.Mileage,
|
||||
Metric = result.Metric,
|
||||
IsRecurring = result.IsRecurring,
|
||||
UseCustomThresholds = result.UseCustomThresholds,
|
||||
CustomThresholds = result.CustomThresholds,
|
||||
ReminderMileageInterval = result.ReminderMileageInterval,
|
||||
ReminderMonthInterval = result.ReminderMonthInterval,
|
||||
CustomMileageInterval = result.CustomMileageInterval,
|
||||
CustomMonthInterval = result.CustomMonthInterval,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_ReminderRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteReminderRecordById(int reminderRecordId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.DeleteReminderRecordById(reminderRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Reminder - Id: {reminderRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Controllers/Vehicle/RepairController.cs
Normal file
101
Controllers/Vehicle/RepairController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetCollisionRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_CollisionRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveCollisionRecordToVehicleId(CollisionRecordInput collisionRecord)
|
||||
{
|
||||
if (collisionRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
|
||||
{
|
||||
Date = DateTime.Parse(collisionRecord.Date),
|
||||
VehicleId = collisionRecord.VehicleId,
|
||||
Mileage = collisionRecord.Mileage,
|
||||
Notes = $"Auto Insert From Repair Record: {collisionRecord.Description}"
|
||||
});
|
||||
}
|
||||
//move files from temp.
|
||||
collisionRecord.Files = collisionRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
if (collisionRecord.Supplies.Any())
|
||||
{
|
||||
collisionRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(collisionRecord.Supplies, DateTime.Parse(collisionRecord.Date), collisionRecord.Description);
|
||||
if (collisionRecord.CopySuppliesAttachment)
|
||||
{
|
||||
collisionRecord.Files.AddRange(GetSuppliesAttachments(collisionRecord.Supplies));
|
||||
}
|
||||
}
|
||||
//push back any reminders
|
||||
if (collisionRecord.ReminderRecordId.Any())
|
||||
{
|
||||
foreach (int reminderRecordId in collisionRecord.ReminderRecordId)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(collisionRecord.Date), collisionRecord.Mileage);
|
||||
}
|
||||
}
|
||||
var result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(collisionRecord.ToCollisionRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), collisionRecord.VehicleId, User.Identity.Name, $"{(collisionRecord.Id == default ? "Created" : "Edited")} Repair Record - Description: {collisionRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddCollisionRecordPartialView()
|
||||
{
|
||||
return PartialView("_CollisionRecordModal", new CollisionRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.RepairRecord).ExtraFields });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetCollisionRecordForEditById(int collisionRecordId)
|
||||
{
|
||||
var result = _collisionRecordDataAccess.GetCollisionRecordById(collisionRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new CollisionRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
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);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteCollisionRecordById(int collisionRecordId)
|
||||
{
|
||||
var result = _collisionRecordDataAccess.DeleteCollisionRecordById(collisionRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Repair Record - Id: {collisionRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
532
Controllers/Vehicle/ReportController.cs
Normal file
532
Controllers/Vehicle/ReportController.cs
Normal file
@@ -0,0 +1,532 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetReportPartialView(int vehicleId)
|
||||
{
|
||||
//get records
|
||||
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
var viewModel = new ReportViewModel();
|
||||
//get totalCostMakeUp
|
||||
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
|
||||
{
|
||||
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
|
||||
GasRecordSum = gasRecords.Sum(x => x.Cost),
|
||||
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost),
|
||||
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
|
||||
};
|
||||
//get costbymonth
|
||||
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
|
||||
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
|
||||
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),
|
||||
DistanceTraveled = x.Max(y => y.DistanceTraveled)
|
||||
}).ToList();
|
||||
//get reminders
|
||||
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
viewModel.ReminderMakeUpForVehicle = new ReminderMakeUpForVehicle
|
||||
{
|
||||
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
|
||||
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
|
||||
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
|
||||
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
|
||||
};
|
||||
//populate year dropdown.
|
||||
var numbersArray = new List<int>();
|
||||
if (serviceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(serviceRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (collisionRecords.Any())
|
||||
{
|
||||
numbersArray.Add(collisionRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (gasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(gasRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (upgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (odometerRecords.Any())
|
||||
{
|
||||
numbersArray.Add(odometerRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
var minYear = numbersArray.Any() ? numbersArray.Min() : DateTime.Now.AddYears(-5).Year;
|
||||
var yearDifference = DateTime.Now.Year - minYear + 1;
|
||||
for (int i = 0; i < yearDifference; i++)
|
||||
{
|
||||
viewModel.Years.Add(DateTime.Now.AddYears(i * -1).Year);
|
||||
}
|
||||
//get collaborators
|
||||
var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId);
|
||||
viewModel.Collaborators = collaborators;
|
||||
//get MPG per month.
|
||||
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
|
||||
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
|
||||
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
|
||||
mileageData.RemoveAll(x => x.MilesPerGallon == default);
|
||||
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
|
||||
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
|
||||
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
Cost = x.Average(y => y.MilesPerGallon)
|
||||
}));
|
||||
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
if (invertedFuelMileageUnit)
|
||||
{
|
||||
foreach(CostForVehicleByMonth monthMileage in monthlyMileageData)
|
||||
{
|
||||
if (monthMileage.Cost != default)
|
||||
{
|
||||
monthMileage.Cost = 100 / monthMileage.Cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
var mpgViewModel = new MPGForVehicleByMonth {
|
||||
CostData = monthlyMileageData,
|
||||
Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit,
|
||||
SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList()
|
||||
};
|
||||
viewModel.FuelMileageForVehicleByMonth = mpgViewModel;
|
||||
return PartialView("_Report", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetCollaboratorsForVehicle(int vehicleId)
|
||||
{
|
||||
var result = _userLogic.GetCollaboratorsForVehicle(vehicleId);
|
||||
return PartialView("_Collaborators", result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult AddCollaboratorsToVehicle(int vehicleId, string username)
|
||||
{
|
||||
var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username);
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult DeleteCollaboratorFromVehicle(int userId, int vehicleId)
|
||||
{
|
||||
var result = _userLogic.DeleteCollaboratorFromVehicle(userId, vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0)
|
||||
{
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (year != default)
|
||||
{
|
||||
serviceRecords.RemoveAll(x => x.Date.Year != year);
|
||||
gasRecords.RemoveAll(x => x.Date.Year != year);
|
||||
collisionRecords.RemoveAll(x => x.Date.Year != year);
|
||||
taxRecords.RemoveAll(x => x.Date.Year != year);
|
||||
upgradeRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
var viewModel = new CostMakeUpForVehicle
|
||||
{
|
||||
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
|
||||
GasRecordSum = gasRecords.Sum(x => x.Cost),
|
||||
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost),
|
||||
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
|
||||
};
|
||||
return PartialView("_CostMakeUpReport", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetCostTableForVehicle(int vehicleId, int year = 0)
|
||||
{
|
||||
var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId);
|
||||
var serviceRecords = vehicleRecords.ServiceRecords;
|
||||
var gasRecords = vehicleRecords.GasRecords;
|
||||
var collisionRecords = vehicleRecords.CollisionRecords;
|
||||
var taxRecords = vehicleRecords.TaxRecords;
|
||||
var upgradeRecords = vehicleRecords.UpgradeRecords;
|
||||
var odometerRecords = vehicleRecords.OdometerRecords;
|
||||
if (year != default)
|
||||
{
|
||||
serviceRecords.RemoveAll(x => x.Date.Year != year);
|
||||
gasRecords.RemoveAll(x => x.Date.Year != year);
|
||||
collisionRecords.RemoveAll(x => x.Date.Year != year);
|
||||
taxRecords.RemoveAll(x => x.Date.Year != year);
|
||||
upgradeRecords.RemoveAll(x => x.Date.Year != year);
|
||||
odometerRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
|
||||
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
|
||||
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
var totalDistanceTraveled = maxMileage - minMileage;
|
||||
var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, serviceRecords, collisionRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords);
|
||||
var viewModel = new CostTableForVehicle
|
||||
{
|
||||
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
|
||||
GasRecordSum = gasRecords.Sum(x => x.Cost),
|
||||
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost),
|
||||
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost),
|
||||
TotalDistance = totalDistanceTraveled,
|
||||
DistanceUnit = vehicleData.UseHours ? "Cost Per Hour" : userConfig.UseMPG ? "Cost Per Mile" : "Cost Per Kilometer",
|
||||
NumberOfDays = totalDays
|
||||
};
|
||||
return PartialView("_CostTableReport", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd)
|
||||
{
|
||||
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
|
||||
var viewModel = new ReminderMakeUpForVehicle
|
||||
{
|
||||
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
|
||||
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
|
||||
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
|
||||
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
|
||||
};
|
||||
return PartialView("_ReminderMakeUpReport", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult GetVehicleAttachments(int vehicleId, List<ImportMode> exportTabs)
|
||||
{
|
||||
List<GenericReportModel> attachmentData = new List<GenericReportModel>();
|
||||
if (exportTabs.Contains(ImportMode.ServiceRecord))
|
||||
{
|
||||
var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.ServiceRecord,
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.RepairRecord))
|
||||
{
|
||||
var records = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.RepairRecord,
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.UpgradeRecord))
|
||||
{
|
||||
var records = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.UpgradeRecord,
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.GasRecord))
|
||||
{
|
||||
var records = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.GasRecord,
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.TaxRecord))
|
||||
{
|
||||
var records = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.TaxRecord,
|
||||
Date = x.Date,
|
||||
Odometer = 0,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.OdometerRecord))
|
||||
{
|
||||
var records = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.OdometerRecord,
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (exportTabs.Contains(ImportMode.NoteRecord))
|
||||
{
|
||||
var records = _noteDataAccess.GetNotesByVehicleId(vehicleId).Where(x => x.Files.Any());
|
||||
attachmentData.AddRange(records.Select(x => new GenericReportModel
|
||||
{
|
||||
DataType = ImportMode.NoteRecord,
|
||||
Date = DateTime.Now,
|
||||
Odometer = 0,
|
||||
Files = x.Files
|
||||
}));
|
||||
}
|
||||
if (attachmentData.Any())
|
||||
{
|
||||
attachmentData = attachmentData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList();
|
||||
var result = _fileHelper.MakeAttachmentsExport(attachmentData);
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage });
|
||||
}
|
||||
return Json(new OperationResponse { Success = true, Message = result });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new OperationResponse { Success = false, Message = "No Attachments Found" });
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
public IActionResult GetVehicleHistory(int vehicleId)
|
||||
{
|
||||
var vehicleHistory = new VehicleHistoryViewModel();
|
||||
vehicleHistory.VehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleId);
|
||||
vehicleHistory.Odometer = maxMileage.ToString("N0");
|
||||
var minMileage = _vehicleLogic.GetMinMileage(vehicleId);
|
||||
var distanceTraveled = maxMileage - minMileage;
|
||||
if (!string.IsNullOrWhiteSpace(vehicleHistory.VehicleData.PurchaseDate))
|
||||
{
|
||||
var endDate = vehicleHistory.VehicleData.SoldDate;
|
||||
int daysOwned = 0;
|
||||
if (string.IsNullOrWhiteSpace(endDate))
|
||||
{
|
||||
endDate = DateTime.Now.ToShortDateString();
|
||||
}
|
||||
try
|
||||
{
|
||||
daysOwned = (DateTime.Parse(endDate) - DateTime.Parse(vehicleHistory.VehicleData.PurchaseDate)).Days;
|
||||
vehicleHistory.DaysOwned = daysOwned.ToString("N0");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
vehicleHistory.DaysOwned = string.Empty;
|
||||
}
|
||||
//calculate depreciation
|
||||
var totalDepreciation = vehicleHistory.VehicleData.PurchasePrice - vehicleHistory.VehicleData.SoldPrice;
|
||||
//we only calculate depreciation if a sold price is provided.
|
||||
if (totalDepreciation != default && vehicleHistory.VehicleData.SoldPrice != default)
|
||||
{
|
||||
vehicleHistory.TotalDepreciation = totalDepreciation;
|
||||
if (daysOwned != default)
|
||||
{
|
||||
vehicleHistory.DepreciationPerDay = Math.Abs(totalDepreciation / daysOwned);
|
||||
}
|
||||
if (distanceTraveled != default)
|
||||
{
|
||||
vehicleHistory.DepreciationPerMile = Math.Abs(totalDepreciation / distanceTraveled);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<GenericReportModel> reportData = new List<GenericReportModel>();
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
||||
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
||||
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
|
||||
vehicleHistory.DistanceUnit = vehicleHistory.VehicleData.UseHours ? "h" : useMPG ? "mi." : "km";
|
||||
vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost);
|
||||
vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost);
|
||||
if (distanceTraveled != default)
|
||||
{
|
||||
vehicleHistory.DistanceTraveled = distanceTraveled.ToString("N0");
|
||||
vehicleHistory.TotalCostPerMile = vehicleHistory.TotalCost / distanceTraveled;
|
||||
vehicleHistory.TotalGasCostPerMile = vehicleHistory.TotalGasCost / distanceTraveled;
|
||||
}
|
||||
var averageMPG = "0";
|
||||
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
|
||||
if (gasViewModels.Any())
|
||||
{
|
||||
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG);
|
||||
}
|
||||
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleHistory.VehicleData.IsElectric, vehicleHistory.VehicleData.UseHours, useMPG, useUKMPG);
|
||||
if (fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l")
|
||||
{
|
||||
//conversion needed.
|
||||
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
|
||||
if (newAverageMPG != 0)
|
||||
{
|
||||
newAverageMPG = 100 / newAverageMPG;
|
||||
}
|
||||
averageMPG = newAverageMPG.ToString("F");
|
||||
fuelEconomyMileageUnit = preferredFuelMileageUnit;
|
||||
}
|
||||
vehicleHistory.MPG = $"{averageMPG} {fuelEconomyMileageUnit}";
|
||||
//insert servicerecords
|
||||
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.ServiceRecord
|
||||
}));
|
||||
//repair records
|
||||
reportData.AddRange(repairRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.RepairRecord
|
||||
}));
|
||||
reportData.AddRange(upgradeRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.UpgradeRecord
|
||||
}));
|
||||
reportData.AddRange(taxRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = 0,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.TaxRecord
|
||||
}));
|
||||
vehicleHistory.VehicleHistory = reportData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList();
|
||||
return PartialView("_VehicleHistory", vehicleHistory);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult GetMonthMPGByVehicle(int vehicleId, int year = 0)
|
||||
{
|
||||
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
|
||||
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
|
||||
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
|
||||
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
|
||||
if (year != 0)
|
||||
{
|
||||
mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year);
|
||||
}
|
||||
mileageData.RemoveAll(x => x.MilesPerGallon == default);
|
||||
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
|
||||
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
Cost = x.Average(y => y.MilesPerGallon)
|
||||
}));
|
||||
monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
if (invertedFuelMileageUnit)
|
||||
{
|
||||
foreach (CostForVehicleByMonth monthMileage in monthlyMileageData)
|
||||
{
|
||||
if (monthMileage.Cost != default)
|
||||
{
|
||||
monthMileage.Cost = 100 / monthMileage.Cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
var mpgViewModel = new MPGForVehicleByMonth
|
||||
{
|
||||
CostData = monthlyMileageData,
|
||||
Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit,
|
||||
SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList()
|
||||
};
|
||||
return PartialView("_MPGByMonthReport", mpgViewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
|
||||
{
|
||||
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
|
||||
if (selectedMetrics.Contains(ImportMode.ServiceRecord))
|
||||
{
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.RepairRecord))
|
||||
{
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetRepairRecordSum(repairRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.UpgradeRecord))
|
||||
{
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.GasRecord))
|
||||
{
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.TaxRecord))
|
||||
{
|
||||
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),
|
||||
DistanceTraveled = x.Max(y => y.DistanceTraveled)
|
||||
}).ToList();
|
||||
return PartialView("_GasCostByMonthReport", groupedRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Controllers/Vehicle/ServiceController.cs
Normal file
101
Controllers/Vehicle/ServiceController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetServiceRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_ServiceRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveServiceRecordToVehicleId(ServiceRecordInput serviceRecord)
|
||||
{
|
||||
if (serviceRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
|
||||
{
|
||||
Date = DateTime.Parse(serviceRecord.Date),
|
||||
VehicleId = serviceRecord.VehicleId,
|
||||
Mileage = serviceRecord.Mileage,
|
||||
Notes = $"Auto Insert From Service Record: {serviceRecord.Description}"
|
||||
});
|
||||
}
|
||||
//move files from temp.
|
||||
serviceRecord.Files = serviceRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
if (serviceRecord.Supplies.Any())
|
||||
{
|
||||
serviceRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(serviceRecord.Supplies, DateTime.Parse(serviceRecord.Date), serviceRecord.Description);
|
||||
if (serviceRecord.CopySuppliesAttachment)
|
||||
{
|
||||
serviceRecord.Files.AddRange(GetSuppliesAttachments(serviceRecord.Supplies));
|
||||
}
|
||||
}
|
||||
//push back any reminders
|
||||
if (serviceRecord.ReminderRecordId.Any())
|
||||
{
|
||||
foreach (int reminderRecordId in serviceRecord.ReminderRecordId)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(serviceRecord.Date), serviceRecord.Mileage);
|
||||
}
|
||||
}
|
||||
var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), serviceRecord.VehicleId, User.Identity.Name, $"{(serviceRecord.Id == default ? "Created" : "Edited")} Service Record - Description: {serviceRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddServiceRecordPartialView()
|
||||
{
|
||||
return PartialView("_ServiceRecordModal", new ServiceRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetServiceRecordForEditById(int serviceRecordId)
|
||||
{
|
||||
var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new ServiceRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
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);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteServiceRecordById(int serviceRecordId)
|
||||
{
|
||||
var result = _serviceRecordDataAccess.DeleteServiceRecordById(serviceRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Service Record - Id: {serviceRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
194
Controllers/Vehicle/SupplyController.cs
Normal file
194
Controllers/Vehicle/SupplyController.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
private List<string> CheckSupplyRecordsAvailability(List<SupplyUsage> supplyUsage)
|
||||
{
|
||||
//returns empty string if all supplies are available
|
||||
var result = new List<string>();
|
||||
foreach (SupplyUsage supply in supplyUsage)
|
||||
{
|
||||
//get supply record.
|
||||
var supplyData = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
|
||||
if (supplyData == null)
|
||||
{
|
||||
result.Add("Missing Supplies, Please Delete This Template and Recreate It.");
|
||||
}
|
||||
else if (supply.Quantity > supplyData.Quantity)
|
||||
{
|
||||
result.Add($"Insufficient Quantity for {supplyData.Description}, need: {supply.Quantity}, available: {supplyData.Quantity}");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private List<UploadedFiles> GetSuppliesAttachments(List<SupplyUsage> supplyUsage)
|
||||
{
|
||||
List<UploadedFiles> results = new List<UploadedFiles>();
|
||||
foreach (SupplyUsage supply in supplyUsage)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId);
|
||||
results.AddRange(result.Files);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private List<SupplyUsageHistory> RequisitionSupplyRecordsByUsage(List<SupplyUsage> supplyUsage, DateTime dateRequisitioned, string usageDescription)
|
||||
{
|
||||
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;
|
||||
//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]
|
||||
public IActionResult GetSupplyRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ToList();
|
||||
}
|
||||
return PartialView("_SupplyRecords", result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetSupplyRecordsForPlanRecordTemplate(int planRecordTemplateId)
|
||||
{
|
||||
var viewModel = new SupplyUsageViewModel();
|
||||
var planRecordTemplate = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId);
|
||||
if (planRecordTemplate != default && planRecordTemplate.VehicleId != default)
|
||||
{
|
||||
var supplies = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(planRecordTemplate.VehicleId);
|
||||
if (_config.GetServerEnableShopSupplies())
|
||||
{
|
||||
supplies.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
|
||||
}
|
||||
supplies.RemoveAll(x => x.Quantity <= 0);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
supplies = supplies.OrderByDescending(x => x.Date).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
supplies = supplies.OrderBy(x => x.Date).ToList();
|
||||
}
|
||||
viewModel.Supplies = supplies;
|
||||
viewModel.Usage = planRecordTemplate.Supplies;
|
||||
}
|
||||
return PartialView("_SupplyUsage", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetSupplyRecordsForRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
|
||||
if (_config.GetServerEnableShopSupplies())
|
||||
{
|
||||
result.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies
|
||||
}
|
||||
result.RemoveAll(x => x.Quantity <= 0);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ToList();
|
||||
}
|
||||
var viewModel = new SupplyUsageViewModel
|
||||
{
|
||||
Supplies = result
|
||||
};
|
||||
return PartialView("_SupplyUsage", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord)
|
||||
{
|
||||
//move files from temp.
|
||||
supplyRecord.Files = supplyRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(supplyRecord.ToSupplyRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), supplyRecord.VehicleId, User.Identity.Name, $"{(supplyRecord.Id == default ? "Created" : "Edited")} Supply Record - Description: {supplyRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddSupplyRecordPartialView()
|
||||
{
|
||||
return PartialView("_SupplyRecordModal", new SupplyRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetSupplyRecordForEditById(int supplyRecordId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new SupplyRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
PartNumber = result.PartNumber,
|
||||
Quantity = result.Quantity,
|
||||
PartSupplier = result.PartSupplier,
|
||||
Notes = result.Notes,
|
||||
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);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteSupplyRecordById(int supplyRecordId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.DeleteSupplyRecordById(supplyRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Supply Record - Id: {supplyRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Controllers/Vehicle/TaxController.cs
Normal file
124
Controllers/Vehicle/TaxController.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetTaxRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ToList();
|
||||
}
|
||||
return PartialView("_TaxRecords", result);
|
||||
}
|
||||
private void UpdateRecurringTaxes(int vehicleId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var recurringFees = result.Where(x => x.IsRecurring);
|
||||
if (recurringFees.Any())
|
||||
{
|
||||
foreach (TaxRecord recurringFee in recurringFees)
|
||||
{
|
||||
var newDate = new DateTime();
|
||||
if (recurringFee.RecurringInterval != ReminderMonthInterval.Other)
|
||||
{
|
||||
newDate = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
newDate = recurringFee.Date.AddMonths(recurringFee.CustomMonthInterval);
|
||||
}
|
||||
if (DateTime.Now > newDate)
|
||||
{
|
||||
recurringFee.IsRecurring = false;
|
||||
var newRecurringFee = new TaxRecord()
|
||||
{
|
||||
VehicleId = recurringFee.VehicleId,
|
||||
Date = newDate,
|
||||
Description = recurringFee.Description,
|
||||
Cost = recurringFee.Cost,
|
||||
IsRecurring = true,
|
||||
Notes = recurringFee.Notes,
|
||||
RecurringInterval = recurringFee.RecurringInterval,
|
||||
CustomMonthInterval = recurringFee.CustomMonthInterval,
|
||||
Files = recurringFee.Files,
|
||||
Tags = recurringFee.Tags,
|
||||
ExtraFields = recurringFee.ExtraFields
|
||||
};
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord)
|
||||
{
|
||||
//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.Any())
|
||||
{
|
||||
foreach (int reminderRecordId in taxRecord.ReminderRecordId)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(taxRecord.Date), null);
|
||||
}
|
||||
}
|
||||
var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), taxRecord.VehicleId, User.Identity.Name, $"{(taxRecord.Id == default ? "Created" : "Edited")} Tax Record - Description: {taxRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddTaxRecordPartialView()
|
||||
{
|
||||
return PartialView("_TaxRecordModal", new TaxRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetTaxRecordForEditById(int taxRecordId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.GetTaxRecordById(taxRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new TaxRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
IsRecurring = result.IsRecurring,
|
||||
RecurringInterval = result.RecurringInterval,
|
||||
CustomMonthInterval = result.CustomMonthInterval,
|
||||
Files = result.Files,
|
||||
Tags = result.Tags,
|
||||
ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields)
|
||||
};
|
||||
return PartialView("_TaxRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteTaxRecordById(int taxRecordId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.DeleteTaxRecordById(taxRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Tax Record - Id: {taxRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Controllers/Vehicle/UpgradeController.cs
Normal file
101
Controllers/Vehicle/UpgradeController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using CarCareTracker.Filter;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public partial class VehicleController
|
||||
{
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_UpgradeRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveUpgradeRecordToVehicleId(UpgradeRecordInput upgradeRecord)
|
||||
{
|
||||
if (upgradeRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert)
|
||||
{
|
||||
_odometerLogic.AutoInsertOdometerRecord(new OdometerRecord
|
||||
{
|
||||
Date = DateTime.Parse(upgradeRecord.Date),
|
||||
VehicleId = upgradeRecord.VehicleId,
|
||||
Mileage = upgradeRecord.Mileage,
|
||||
Notes = $"Auto Insert From Upgrade Record: {upgradeRecord.Description}"
|
||||
});
|
||||
}
|
||||
//move files from temp.
|
||||
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
if (upgradeRecord.Supplies.Any())
|
||||
{
|
||||
upgradeRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Description);
|
||||
if (upgradeRecord.CopySuppliesAttachment)
|
||||
{
|
||||
upgradeRecord.Files.AddRange(GetSuppliesAttachments(upgradeRecord.Supplies));
|
||||
}
|
||||
}
|
||||
//push back any reminders
|
||||
if (upgradeRecord.ReminderRecordId.Any())
|
||||
{
|
||||
foreach (int reminderRecordId in upgradeRecord.ReminderRecordId)
|
||||
{
|
||||
PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Mileage);
|
||||
}
|
||||
}
|
||||
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), upgradeRecord.VehicleId, User.Identity.Name, $"{(upgradeRecord.Id == default ? "Created" : "Edited")} Upgrade Record - Description: {upgradeRecord.Description}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddUpgradeRecordPartialView()
|
||||
{
|
||||
return PartialView("_UpgradeRecordModal", new UpgradeRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetUpgradeRecordForEditById(int upgradeRecordId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new UpgradeRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
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);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteUpgradeRecordById(int upgradeRecordId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(upgradeRecordId);
|
||||
if (result)
|
||||
{
|
||||
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), 0, User.Identity.Name, $"Deleted Upgrade Record - Id: {upgradeRecordId}");
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
9
Enum/DashboardMetric.cs
Normal file
9
Enum/DashboardMetric.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum DashboardMetric
|
||||
{
|
||||
Default = 0,
|
||||
TotalCost = 1,
|
||||
CostPerMile = 2
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CarCareTracker.External.Implementations
|
||||
try
|
||||
{
|
||||
//create table if not exist.
|
||||
string initCMD = $"CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)";
|
||||
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)";
|
||||
using (var ctext = pgDataSource.CreateCommand(initCMD))
|
||||
{
|
||||
ctext.ExecuteNonQuery();
|
||||
|
||||
@@ -12,10 +12,12 @@ namespace CarCareTracker.Helper
|
||||
UserConfig GetUserConfig(ClaimsPrincipal user);
|
||||
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
|
||||
bool AuthenticateRootUser(string username, string password);
|
||||
bool AuthenticateRootUserOIDC(string email);
|
||||
string GetWebHookUrl();
|
||||
string GetMOTD();
|
||||
string GetLogoUrl();
|
||||
string GetServerLanguage();
|
||||
bool GetServerDisabledRegistration();
|
||||
bool GetServerEnableShopSupplies();
|
||||
string GetServerPostgresConnection();
|
||||
string GetAllowedFileUploadExtensions();
|
||||
@@ -89,11 +91,26 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return username == rootUsername && password == rootPassword;
|
||||
}
|
||||
public bool AuthenticateRootUserOIDC(string email)
|
||||
{
|
||||
var rootEmail = _config[nameof(UserConfig.DefaultReminderEmail)] ?? string.Empty;
|
||||
var rootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]);
|
||||
if (!rootUserOIDC || string.IsNullOrWhiteSpace(rootEmail))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return email == rootEmail;
|
||||
}
|
||||
public string GetServerLanguage()
|
||||
{
|
||||
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
|
||||
return serverLanguage;
|
||||
}
|
||||
public bool GetServerDisabledRegistration()
|
||||
{
|
||||
var registrationDisabled = bool.Parse(_config[nameof(UserConfig.DisableRegistration)]);
|
||||
return registrationDisabled;
|
||||
}
|
||||
public string GetServerPostgresConnection()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_config["POSTGRES_CONNECTION"]))
|
||||
@@ -162,9 +179,11 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
|
||||
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
|
||||
UseSystemColorMode = bool.Parse(_config[nameof(UserConfig.UseSystemColorMode)]),
|
||||
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
|
||||
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
|
||||
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
|
||||
EnableRootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]),
|
||||
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
|
||||
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
|
||||
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
|
||||
@@ -178,9 +197,12 @@ namespace CarCareTracker.Helper
|
||||
EnableShopSupplies = bool.Parse(_config[nameof(UserConfig.EnableShopSupplies)]),
|
||||
EnableExtraFieldColumns = bool.Parse(_config[nameof(UserConfig.EnableExtraFieldColumns)]),
|
||||
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>(),
|
||||
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>(),
|
||||
UserColumnPreferences = _config.GetSection(nameof(UserConfig.UserColumnPreferences)).Get<List<UserColumnPreference>>() ?? new List<UserColumnPreference>(),
|
||||
ReminderUrgencyConfig = _config.GetSection(nameof(UserConfig.ReminderUrgencyConfig)).Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig(),
|
||||
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
|
||||
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)]),
|
||||
DefaultReminderEmail = _config[nameof(UserConfig.DefaultReminderEmail)],
|
||||
DisableRegistration = bool.Parse(_config[nameof(UserConfig.DisableRegistration)])
|
||||
};
|
||||
int userId = 0;
|
||||
if (user != null)
|
||||
|
||||
@@ -57,6 +57,10 @@ namespace CarCareTracker.Helper
|
||||
if (i > 0)
|
||||
{
|
||||
var deltaMileage = currentObject.Mileage - previousMileage;
|
||||
if (deltaMileage < 0)
|
||||
{
|
||||
deltaMileage = 0;
|
||||
}
|
||||
var gasRecordViewModel = new GasRecordViewModel()
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
|
||||
@@ -109,18 +109,24 @@ namespace CarCareTracker.Helper
|
||||
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
|
||||
//construct html table.
|
||||
string emailBody = File.ReadAllText(emailTemplatePath);
|
||||
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}");
|
||||
emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}");
|
||||
string tableBody = "";
|
||||
foreach(ReminderRecordViewModel reminder in reminders)
|
||||
{
|
||||
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
|
||||
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
|
||||
tableBody += $"<tr class='{reminder.Urgency}'><td>{StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency)}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
|
||||
}
|
||||
emailBody = emailBody.Replace("{TableBody}", tableBody);
|
||||
try
|
||||
{
|
||||
SendEmail(emailAddresses, emailSubject, emailBody);
|
||||
return new OperationResponse { Success = true, Message = "Email Sent!" };
|
||||
var result = SendEmail(emailAddresses, emailSubject, emailBody);
|
||||
if (result)
|
||||
{
|
||||
return new OperationResponse { Success = true, Message = "Email Sent!" };
|
||||
} else
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = ex.Message };
|
||||
|
||||
@@ -66,6 +66,10 @@ namespace CarCareTracker.Helper
|
||||
var reminderUrgencyConfig = _config.GetReminderUrgencyConfig();
|
||||
foreach (var reminder in reminders)
|
||||
{
|
||||
if (reminder.UseCustomThresholds)
|
||||
{
|
||||
reminderUrgencyConfig = reminder.CustomThresholds;
|
||||
}
|
||||
var reminderViewModel = new ReminderRecordViewModel()
|
||||
{
|
||||
Id = reminder.Id,
|
||||
|
||||
@@ -9,13 +9,15 @@ namespace CarCareTracker.Helper
|
||||
/// </summary>
|
||||
public static class StaticHelper
|
||||
{
|
||||
public static string VersionNumber = "1.3.6";
|
||||
public static string VersionNumber = "1.3.9";
|
||||
public static string DbName = "data/cartracker.db";
|
||||
public static string UserConfigPath = "config/userConfig.json";
|
||||
public static string GenericErrorMessage = "An error occurred, please try again later";
|
||||
public static string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
|
||||
public static string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
|
||||
public static string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
|
||||
public static string TranslationPath = "https://hargata.github.io/lubelog_translations";
|
||||
public static string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
|
||||
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
|
||||
{
|
||||
switch (input)
|
||||
@@ -119,6 +121,10 @@ namespace CarCareTracker.Helper
|
||||
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
|
||||
};
|
||||
}
|
||||
public static List<string> GetBarChartColors()
|
||||
{
|
||||
return new List<string> { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
|
||||
}
|
||||
|
||||
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
|
||||
{
|
||||
@@ -199,6 +205,8 @@ namespace CarCareTracker.Helper
|
||||
recordExtraFields.Add(extraField);
|
||||
}
|
||||
}
|
||||
//re-order extra fields
|
||||
recordExtraFields = recordExtraFields.OrderBy(x => templateExtraFields.FindIndex(y => y.Name == x.Name)).ToList();
|
||||
return recordExtraFields;
|
||||
}
|
||||
|
||||
@@ -244,6 +252,10 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
var motd = config["LUBELOGGER_MOTD"] ?? "Not Configured";
|
||||
Console.WriteLine($"Message Of The Day: {motd}");
|
||||
if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name))
|
||||
{
|
||||
Console.WriteLine("No Locale or Culture Configured for LubeLogger, Check Environment Variables");
|
||||
}
|
||||
}
|
||||
public static async void NotifyAsync(string webhookURL, int vehicleId, string username, string action)
|
||||
{
|
||||
@@ -288,6 +300,85 @@ namespace CarCareTracker.Helper
|
||||
return "bi-file-bar-graph";
|
||||
}
|
||||
}
|
||||
public static string GetVehicleIdentifier(Vehicle vehicle)
|
||||
{
|
||||
if (vehicle.VehicleIdentifier == "LicensePlate")
|
||||
{
|
||||
return vehicle.LicensePlate;
|
||||
} else
|
||||
{
|
||||
if (vehicle.ExtraFields.Any(x=>x.Name == vehicle.VehicleIdentifier))
|
||||
{
|
||||
return vehicle.ExtraFields?.FirstOrDefault(x=>x.Name == vehicle.VehicleIdentifier)?.Value;
|
||||
} else
|
||||
{
|
||||
return "N/A";
|
||||
}
|
||||
}
|
||||
}
|
||||
public static string GetVehicleIdentifier(VehicleViewModel vehicle)
|
||||
{
|
||||
if (vehicle.VehicleIdentifier == "LicensePlate")
|
||||
{
|
||||
return vehicle.LicensePlate;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier))
|
||||
{
|
||||
return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "N/A";
|
||||
}
|
||||
}
|
||||
}
|
||||
//Translations
|
||||
public static string GetTranslationDownloadPath(string continent, string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name)){
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (continent)
|
||||
{
|
||||
case "NorthAmerica":
|
||||
continent = "North America";
|
||||
break;
|
||||
case "SouthAmerica":
|
||||
continent = "South America";
|
||||
break;
|
||||
}
|
||||
return $"{TranslationPath}/{continent}/{name}.json";
|
||||
}
|
||||
}
|
||||
public static string GetTranslationName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return string.Empty;
|
||||
} else
|
||||
{
|
||||
try
|
||||
{
|
||||
string cleanedName = name.Contains("_") ? name.Replace("_", "-") : name;
|
||||
string displayName = CultureInfo.GetCultureInfo(cleanedName).DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
else
|
||||
{
|
||||
return displayName;
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
//CSV Write Methods
|
||||
public static void WriteGenericRecordExportModel(CsvWriter _csv, IEnumerable<GenericRecordExportModel> genericRecords)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
@@ -6,17 +7,22 @@ namespace CarCareTracker.Helper
|
||||
public interface ITranslationHelper
|
||||
{
|
||||
string Translate(string userLanguage, string text);
|
||||
Dictionary<string, string> GetTranslations(string userLanguage);
|
||||
OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations);
|
||||
string ExportTranslation(Dictionary<string, string> translations);
|
||||
}
|
||||
public class TranslationHelper : ITranslationHelper
|
||||
{
|
||||
private readonly IFileHelper _fileHelper;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<ITranslationHelper> _logger;
|
||||
private IMemoryCache _cache;
|
||||
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache)
|
||||
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache, ILogger<ITranslationHelper> logger)
|
||||
{
|
||||
_fileHelper = fileHelper;
|
||||
_config = config;
|
||||
_cache = memoryCache;
|
||||
_logger = logger;
|
||||
}
|
||||
public string Translate(string userLanguage, string text)
|
||||
{
|
||||
@@ -36,11 +42,13 @@ namespace CarCareTracker.Helper
|
||||
return translationDictionary ?? new Dictionary<string, string>();
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError($"Could not find translation file for {userLanguage}");
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
});
|
||||
@@ -52,10 +60,121 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
//create entry
|
||||
dictionary.Add(translationKey, text);
|
||||
_logger.LogInformation($"Translation key added to {userLanguage} for {translationKey} with value {text}");
|
||||
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
private Dictionary<string, string> GetDefaultTranslation()
|
||||
{
|
||||
//this method always returns en_US translation.
|
||||
var translationFilePath = _fileHelper.GetFullFilePath($"/defaults/en_US.json");
|
||||
if (!string.IsNullOrWhiteSpace(translationFilePath))
|
||||
{
|
||||
//file exists.
|
||||
try
|
||||
{
|
||||
var translationFile = File.ReadAllText(translationFilePath);
|
||||
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
|
||||
return translationDictionary ?? new Dictionary<string, string>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
_logger.LogError($"Could not find translation file for en_US");
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
public Dictionary<string, string> GetTranslations(string userLanguage)
|
||||
{
|
||||
var defaultTranslation = GetDefaultTranslation();
|
||||
if (userLanguage == "en_US")
|
||||
{
|
||||
return defaultTranslation;
|
||||
}
|
||||
var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json");
|
||||
if (!string.IsNullOrWhiteSpace(translationFilePath))
|
||||
{
|
||||
//file exists.
|
||||
try
|
||||
{
|
||||
var translationFile = File.ReadAllText(translationFilePath);
|
||||
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
|
||||
if (translationDictionary != null)
|
||||
{
|
||||
foreach(var translation in translationDictionary)
|
||||
{
|
||||
defaultTranslation[translation.Key] = translation.Value;
|
||||
}
|
||||
}
|
||||
return defaultTranslation ?? new Dictionary<string, string>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
_logger.LogError($"Could not find translation file for {userLanguage}");
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
public OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations)
|
||||
{
|
||||
if (userLanguage == "en_US")
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "The translation file name en_US is reserved." };
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(userLanguage))
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "File name is not provided." };
|
||||
}
|
||||
if (!translations.Any())
|
||||
{
|
||||
return new OperationResponse { Success = false, Message = "Translation has no data." };
|
||||
}
|
||||
var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false);
|
||||
try
|
||||
{
|
||||
if (File.Exists(translationFilePath))
|
||||
{
|
||||
//write to file
|
||||
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
|
||||
_cache.Remove($"lang_{userLanguage}"); //clear out cache, force a reload from file.
|
||||
} else
|
||||
{
|
||||
//write to file
|
||||
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
|
||||
}
|
||||
return new OperationResponse { Success = true, Message = "Translation Updated" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
|
||||
}
|
||||
}
|
||||
public string ExportTranslation(Dictionary<string, string> translations)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempFileName = $"/temp/{Guid.NewGuid()}.json";
|
||||
string uploadDirectory = _fileHelper.GetFullFilePath("temp/", false);
|
||||
if (!Directory.Exists(uploadDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(uploadDirectory);
|
||||
}
|
||||
var saveFilePath = _fileHelper.GetFullFilePath(tempFileName, false);
|
||||
File.WriteAllText(saveFilePath, JsonSerializer.Serialize(translations));
|
||||
return tempFileName;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,14 +245,7 @@ namespace CarCareTracker.Logic
|
||||
{
|
||||
if (UserIsRoot(credentials))
|
||||
{
|
||||
return new UserData()
|
||||
{
|
||||
Id = -1,
|
||||
UserName = credentials.UserName,
|
||||
IsAdmin = true,
|
||||
IsRootUser = true,
|
||||
EmailAddress = string.Empty
|
||||
};
|
||||
return GetRootUserData(credentials.UserName);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -271,6 +264,13 @@ namespace CarCareTracker.Logic
|
||||
}
|
||||
public UserData ValidateOpenIDUser(LoginModel credentials)
|
||||
{
|
||||
//validate for root user
|
||||
var isRootUser = _configHelper.AuthenticateRootUserOIDC(credentials.EmailAddress);
|
||||
if (isRootUser)
|
||||
{
|
||||
return GetRootUserData(credentials.EmailAddress);
|
||||
}
|
||||
|
||||
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
|
||||
if (result.Id != default)
|
||||
{
|
||||
@@ -420,6 +420,17 @@ namespace CarCareTracker.Logic
|
||||
var hashedPassword = GetHash(credentials.Password);
|
||||
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
|
||||
}
|
||||
private UserData GetRootUserData(string username)
|
||||
{
|
||||
return new UserData()
|
||||
{
|
||||
Id = -1,
|
||||
UserName = username,
|
||||
IsAdmin = true,
|
||||
IsRootUser = true,
|
||||
EmailAddress = string.Empty
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
private static string GetHash(string value)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace CarCareTracker.Logic
|
||||
}
|
||||
public bool AutoInsertOdometerRecord(OdometerRecord odometer)
|
||||
{
|
||||
if (odometer.Mileage == default)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var lastReportedMileage = GetLastOdometerRecordMileage(odometer.VehicleId, new List<OdometerRecord>());
|
||||
odometer.InitialMileage = lastReportedMileage != default ? lastReportedMileage : odometer.Mileage;
|
||||
|
||||
|
||||
@@ -6,9 +6,14 @@ namespace CarCareTracker.Logic
|
||||
{
|
||||
public interface IVehicleLogic
|
||||
{
|
||||
VehicleRecords GetVehicleRecords(int vehicleId);
|
||||
decimal GetVehicleTotalCost(VehicleRecords vehicleRecords);
|
||||
int GetMaxMileage(int vehicleId);
|
||||
int GetMaxMileage(VehicleRecords vehicleRecords);
|
||||
int GetMinMileage(int vehicleId);
|
||||
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId);
|
||||
int GetMinMileage(VehicleRecords vehicleRecords);
|
||||
int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords);
|
||||
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage);
|
||||
}
|
||||
public class VehicleLogic: IVehicleLogic
|
||||
{
|
||||
@@ -16,6 +21,7 @@ namespace CarCareTracker.Logic
|
||||
private readonly IGasRecordDataAccess _gasRecordDataAccess;
|
||||
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
|
||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
|
||||
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
@@ -24,6 +30,7 @@ namespace CarCareTracker.Logic
|
||||
IGasRecordDataAccess gasRecordDataAccess,
|
||||
ICollisionRecordDataAccess collisionRecordDataAccess,
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
||||
ITaxRecordDataAccess taxRecordDataAccess,
|
||||
IOdometerRecordDataAccess odometerRecordDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IReminderHelper reminderHelper
|
||||
@@ -32,10 +39,32 @@ namespace CarCareTracker.Logic
|
||||
_gasRecordDataAccess = gasRecordDataAccess;
|
||||
_collisionRecordDataAccess = collisionRecordDataAccess;
|
||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||
_taxRecordDataAccess = taxRecordDataAccess;
|
||||
_odometerRecordDataAccess = odometerRecordDataAccess;
|
||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||
_reminderHelper = reminderHelper;
|
||||
}
|
||||
public VehicleRecords GetVehicleRecords(int vehicleId)
|
||||
{
|
||||
return new VehicleRecords
|
||||
{
|
||||
ServiceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId),
|
||||
GasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId),
|
||||
CollisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId),
|
||||
TaxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId),
|
||||
UpgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId),
|
||||
OdometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId),
|
||||
};
|
||||
}
|
||||
public decimal GetVehicleTotalCost(VehicleRecords vehicleRecords)
|
||||
{
|
||||
var serviceRecordSum = vehicleRecords.ServiceRecords.Sum(x => x.Cost);
|
||||
var repairRecordSum = vehicleRecords.CollisionRecords.Sum(x => x.Cost);
|
||||
var upgradeRecordSum = vehicleRecords.UpgradeRecords.Sum(x => x.Cost);
|
||||
var taxRecordSum = vehicleRecords.TaxRecords.Sum(x => x.Cost);
|
||||
var gasRecordSum = vehicleRecords.GasRecords.Sum(x => x.Cost);
|
||||
return serviceRecordSum + repairRecordSum + upgradeRecordSum + taxRecordSum + gasRecordSum;
|
||||
}
|
||||
public int GetMaxMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
@@ -66,6 +95,31 @@ namespace CarCareTracker.Logic
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
public int GetMaxMileage(VehicleRecords vehicleRecords)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
if (vehicleRecords.ServiceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(vehicleRecords.ServiceRecords.Max(x => x.Mileage));
|
||||
}
|
||||
if (vehicleRecords.CollisionRecords.Any())
|
||||
{
|
||||
numbersArray.Add(vehicleRecords.CollisionRecords.Max(x => x.Mileage));
|
||||
}
|
||||
if (vehicleRecords.GasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(vehicleRecords.GasRecords.Max(x => x.Mileage));
|
||||
}
|
||||
if (vehicleRecords.UpgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(vehicleRecords.UpgradeRecords.Max(x => x.Mileage));
|
||||
}
|
||||
if (vehicleRecords.OdometerRecords.Any())
|
||||
{
|
||||
numbersArray.Add(vehicleRecords.OdometerRecords.Max(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
public int GetMinMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
@@ -96,9 +150,70 @@ namespace CarCareTracker.Logic
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Min() : 0;
|
||||
}
|
||||
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId)
|
||||
public int GetMinMileage(VehicleRecords vehicleRecords)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
var _serviceRecords = vehicleRecords.ServiceRecords.Where(x => x.Mileage != default).ToList();
|
||||
if (_serviceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(_serviceRecords.Min(x => x.Mileage));
|
||||
}
|
||||
var _repairRecords = vehicleRecords.CollisionRecords.Where(x => x.Mileage != default).ToList();
|
||||
if (_repairRecords.Any())
|
||||
{
|
||||
numbersArray.Add(_repairRecords.Min(x => x.Mileage));
|
||||
}
|
||||
var _gasRecords = vehicleRecords.GasRecords.Where(x => x.Mileage != default).ToList();
|
||||
if (_gasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(_gasRecords.Min(x => x.Mileage));
|
||||
}
|
||||
var _upgradeRecords = vehicleRecords.UpgradeRecords.Where(x => x.Mileage != default).ToList();
|
||||
if (_upgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(_upgradeRecords.Min(x => x.Mileage));
|
||||
}
|
||||
var _odometerRecords = vehicleRecords.OdometerRecords.Where(x => x.Mileage != default).ToList();
|
||||
if (_odometerRecords.Any())
|
||||
{
|
||||
numbersArray.Add(_odometerRecords.Min(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Min() : 0;
|
||||
}
|
||||
public int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords)
|
||||
{
|
||||
var startDate = DateTime.Now;
|
||||
var endDate = DateTime.Now;
|
||||
if (!string.IsNullOrWhiteSpace(soldDate))
|
||||
{
|
||||
endDate = DateTime.Parse(soldDate);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(purchaseDate))
|
||||
{
|
||||
//if purchase date is provided, then we just have to subtract the begin date to end date and return number of months
|
||||
startDate = DateTime.Parse(purchaseDate);
|
||||
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
|
||||
return timeElapsed;
|
||||
}
|
||||
var dateArray = new List<DateTime>();
|
||||
dateArray.AddRange(serviceRecords.Select(x => x.Date));
|
||||
dateArray.AddRange(repairRecords.Select(x => x.Date));
|
||||
dateArray.AddRange(gasRecords.Select(x => x.Date));
|
||||
dateArray.AddRange(upgradeRecords.Select(x => x.Date));
|
||||
dateArray.AddRange(odometerRecords.Select(x => x.Date));
|
||||
dateArray.AddRange(taxRecords.Select(x => x.Date));
|
||||
if (dateArray.Any())
|
||||
{
|
||||
startDate = dateArray.Min();
|
||||
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
|
||||
return timeElapsed;
|
||||
} else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage)
|
||||
{
|
||||
var currentMileage = GetMaxMileage(vehicleId);
|
||||
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
|
||||
return results.Any(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
|
||||
|
||||
27
Models/API/VehicleInfo.cs
Normal file
27
Models/API/VehicleInfo.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class VehicleInfo
|
||||
{
|
||||
public Vehicle VehicleData { get; set; } = new Vehicle();
|
||||
public int VeryUrgentReminderCount { get; set; }
|
||||
public int UrgentReminderCount { get; set;}
|
||||
public int NotUrgentReminderCount { get; set; }
|
||||
public int PastDueReminderCount { get; set; }
|
||||
public ReminderExportModel NextReminder { get; set; }
|
||||
public int ServiceRecordCount { get; set; }
|
||||
public decimal ServiceRecordCost { get; set; }
|
||||
public int RepairRecordCount { get; set; }
|
||||
public decimal RepairRecordCost { get; set; }
|
||||
public int UpgradeRecordCount { get; set; }
|
||||
public decimal UpgradeRecordCost { get; set; }
|
||||
public int TaxRecordCount { get; set; }
|
||||
public decimal TaxRecordCost { get; set; }
|
||||
public int GasRecordCount { get; set; }
|
||||
public decimal GasRecordCost { get; set; }
|
||||
public int LastReportedOdometer { get; set; }
|
||||
public int PlanRecordBackLogCount { get; set; }
|
||||
public int PlanRecordInProgressCount { get; set; }
|
||||
public int PlanRecordTestingCount { get; set; }
|
||||
public int PlanRecordDoneCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public bool UseCustomThresholds { get; set; } = false;
|
||||
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
|
||||
public int CustomMileageInterval { get; set; } = 0;
|
||||
public int CustomMonthInterval { get; set; } = 0;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public bool UseCustomThresholds { get; set; } = false;
|
||||
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
|
||||
public int CustomMileageInterval { get; set; } = 0;
|
||||
public int CustomMonthInterval { get; set; } = 0;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
@@ -26,6 +28,8 @@
|
||||
Description = Description,
|
||||
Metric = Metric,
|
||||
IsRecurring = IsRecurring,
|
||||
UseCustomThresholds = UseCustomThresholds,
|
||||
CustomThresholds = CustomThresholds,
|
||||
ReminderMileageInterval = ReminderMileageInterval,
|
||||
ReminderMonthInterval = ReminderMonthInterval,
|
||||
CustomMileageInterval = CustomMileageInterval,
|
||||
|
||||
27
Models/Report/CostTableForVehicle.cs
Normal file
27
Models/Report/CostTableForVehicle.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class CostTableForVehicle
|
||||
{
|
||||
public string DistanceUnit { get; set; } = "Cost Per Mile";
|
||||
public int TotalDistance { get; set; }
|
||||
public int NumberOfDays { get; set; }
|
||||
public decimal ServiceRecordSum { get; set; }
|
||||
public decimal GasRecordSum { get; set; }
|
||||
public decimal TaxRecordSum { get; set; }
|
||||
public decimal CollisionRecordSum { get; set; }
|
||||
public decimal UpgradeRecordSum { get; set; }
|
||||
public decimal ServiceRecordPerMile { get { return TotalDistance != default ? ServiceRecordSum / TotalDistance : 0; } }
|
||||
public decimal GasRecordPerMile { get { return TotalDistance != default ? GasRecordSum / TotalDistance : 0; } }
|
||||
public decimal CollisionRecordPerMile { get { return TotalDistance != default ? CollisionRecordSum / TotalDistance : 0; } }
|
||||
public decimal UpgradeRecordPerMile { get { return TotalDistance != default ? UpgradeRecordSum / TotalDistance : 0; } }
|
||||
public decimal TaxRecordPerMile { get { return TotalDistance != default ? TaxRecordSum / TotalDistance : 0; } }
|
||||
public decimal ServiceRecordPerDay { get { return NumberOfDays != default ? ServiceRecordSum / NumberOfDays : 0; } }
|
||||
public decimal GasRecordPerDay { get { return NumberOfDays != default ? GasRecordSum / NumberOfDays : 0; } }
|
||||
public decimal CollisionRecordPerDay { get { return NumberOfDays != default ? CollisionRecordSum / NumberOfDays : 0; } }
|
||||
public decimal UpgradeRecordPerDay { get { return NumberOfDays != default ? UpgradeRecordSum / NumberOfDays : 0; } }
|
||||
public decimal TaxRecordPerDay { get { return NumberOfDays != default ? TaxRecordSum / NumberOfDays : 0; } }
|
||||
public decimal TotalPerDay { get { return ServiceRecordPerDay + CollisionRecordPerDay + UpgradeRecordPerDay + GasRecordPerDay + TaxRecordPerDay; } }
|
||||
public decimal TotalPerMile { get { return ServiceRecordPerMile + CollisionRecordPerMile + UpgradeRecordPerMile + GasRecordPerMile + TaxRecordPerMile; } }
|
||||
public decimal TotalCost { get { return ServiceRecordSum + CollisionRecordSum + UpgradeRecordSum + GasRecordSum + TaxRecordSum; } }
|
||||
}
|
||||
}
|
||||
9
Models/Report/MPGForVehicleByMonth.cs
Normal file
9
Models/Report/MPGForVehicleByMonth.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class MPGForVehicleByMonth
|
||||
{
|
||||
public List<CostForVehicleByMonth> CostData { get; set; } = new List<CostForVehicleByMonth>();
|
||||
public List<CostForVehicleByMonth> SortedCostData { get; set; } = new List<CostForVehicleByMonth>();
|
||||
public string Unit { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
public class ReportViewModel
|
||||
{
|
||||
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
|
||||
public List<CostForVehicleByMonth> FuelMileageForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
|
||||
public MPGForVehicleByMonth FuelMileageForVehicleByMonth { get; set; } = new MPGForVehicleByMonth();
|
||||
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
|
||||
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
|
||||
public List<int> Years { get; set; } = new List<int>();
|
||||
|
||||
@@ -4,6 +4,5 @@ namespace CarCareTracker.Models
|
||||
{
|
||||
public UserConfig UserConfig { get; set; }
|
||||
public List<string> UILanguages { get; set; }
|
||||
public Sponsors Sponsors { get; set; } = new Sponsors();
|
||||
}
|
||||
}
|
||||
|
||||
12
Models/Shared/VehicleRecords.cs
Normal file
12
Models/Shared/VehicleRecords.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class VehicleRecords
|
||||
{
|
||||
public List<ServiceRecord> ServiceRecords { get; set; } = new List<ServiceRecord>();
|
||||
public List<CollisionRecord> CollisionRecords { get; set; } = new List<CollisionRecord>();
|
||||
public List<UpgradeRecord> UpgradeRecords { get; set; } = new List<UpgradeRecord>();
|
||||
public List<GasRecord> GasRecords { get; set; } = new List<GasRecord>();
|
||||
public List<TaxRecord> TaxRecords { get; set; } = new List<TaxRecord>();
|
||||
public List<OdometerRecord> OdometerRecords { get; set; } = new List<OdometerRecord>();
|
||||
}
|
||||
}
|
||||
12
Models/Translations.cs
Normal file
12
Models/Translations.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class Translations
|
||||
{
|
||||
public List<string> Africa { get; set; } = new List<string>();
|
||||
public List<string> Asia { get; set; } = new List<string>();
|
||||
public List<string> Europe { get; set; } = new List<string>();
|
||||
public List<string> NorthAmerica { get; set; } = new List<string>();
|
||||
public List<string> SouthAmerica { get; set; } = new List<string>();
|
||||
public List<string> Oceania { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@
|
||||
public class UserConfig
|
||||
{
|
||||
public bool UseDarkMode { get; set; }
|
||||
public bool UseSystemColorMode { get; set; }
|
||||
public bool EnableCsvImports { get; set; }
|
||||
public bool UseMPG { get; set; }
|
||||
public bool UseDescending { get; set; }
|
||||
public bool EnableAuth { get; set; }
|
||||
public bool DisableRegistration { get; set; }
|
||||
public bool EnableRootUserOIDC { get; set; }
|
||||
public bool HideZero { get; set; }
|
||||
public bool UseUKMPG {get;set;}
|
||||
public bool UseThreeDecimalGasCost { get; set; }
|
||||
@@ -22,6 +25,7 @@
|
||||
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
|
||||
public string UserNameHash { get; set; }
|
||||
public string UserPasswordHash { get; set;}
|
||||
public string DefaultReminderEmail { get; set; } = string.Empty;
|
||||
public string UserLanguage { get; set; } = "en_US";
|
||||
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
|
||||
ImportMode.Dashboard,
|
||||
@@ -31,7 +35,21 @@
|
||||
ImportMode.UpgradeRecord,
|
||||
ImportMode.TaxRecord,
|
||||
ImportMode.ReminderRecord,
|
||||
ImportMode.NoteRecord};
|
||||
ImportMode.NoteRecord
|
||||
};
|
||||
public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard;
|
||||
public List<ImportMode> TabOrder { get; set; } = new List<ImportMode>() {
|
||||
ImportMode.Dashboard,
|
||||
ImportMode.PlanRecord,
|
||||
ImportMode.OdometerRecord,
|
||||
ImportMode.ServiceRecord,
|
||||
ImportMode.RepairRecord,
|
||||
ImportMode.UpgradeRecord,
|
||||
ImportMode.GasRecord,
|
||||
ImportMode.SupplyRecord,
|
||||
ImportMode.TaxRecord,
|
||||
ImportMode.NoteRecord,
|
||||
ImportMode.ReminderRecord
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
public bool IsElectric { get; set; } = false;
|
||||
public bool IsDiesel { get; set; } = false;
|
||||
public bool UseHours { get; set; } = false;
|
||||
public bool OdometerOptional { get; set; } = false;
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public bool HasOdometerAdjustment { get; set; } = false;
|
||||
@@ -26,5 +27,10 @@
|
||||
/// Primarily used for vehicles where the odometer does not reflect actual mileage.
|
||||
/// </summary>
|
||||
public string OdometerDifference { get; set; } = "0";
|
||||
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
|
||||
/// <summary>
|
||||
/// Determines what is displayed in place of the license plate.
|
||||
/// </summary>
|
||||
public string VehicleIdentifier { get; set; } = "LicensePlate";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,16 @@
|
||||
public bool IsElectric { get; set; } = false;
|
||||
public bool IsDiesel { get; set; } = false;
|
||||
public bool UseHours { get; set; } = false;
|
||||
public bool OdometerOptional { get; set; } = false;
|
||||
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public int LastReportedMileage;
|
||||
public bool HasReminders = false;
|
||||
public string VehicleIdentifier { get; set; } = "LicensePlate";
|
||||
//Dashboard Metric Attributes
|
||||
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
|
||||
public int LastReportedMileage { get; set; }
|
||||
public bool HasReminders { get; set; } = false;
|
||||
public decimal CostPerMile { get; set; }
|
||||
public decimal TotalCost { get; set; }
|
||||
public string DistanceUnit { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ Try it out before you download it! The live demo resets every 20 minutes.
|
||||
## Download
|
||||
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
|
||||
|
||||
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
|
||||
Read this [Getting Started Guide](https://docs.lubelogger.com/Installation/Getting%20Started) on how to download either of them
|
||||
|
||||
### Kubernetes Deployment
|
||||
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
|
||||
@@ -28,7 +28,7 @@ Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started)
|
||||
### Need Help?
|
||||
[Documentation](https://docs.lubelogger.com/)
|
||||
|
||||
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
|
||||
[Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
|
||||
|
||||
[Search Existing Issues](https://github.com/hargata/lubelog/issues)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger API";
|
||||
ViewData["Title"] = "API";
|
||||
}
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center">
|
||||
@@ -26,7 +26,7 @@
|
||||
<h6>Parameters</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -40,7 +40,37 @@
|
||||
No Params
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5 copyable">
|
||||
<code>/api/vehicle/info</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns details for list of vehicles or specific vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
VehicleId - Id of Vehicle(optional)
|
||||
</div>
|
||||
</div>
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5 copyable">
|
||||
<code>/api/vehicle/adjustedodometer</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns odometer reading with adjustments applied
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
<br />
|
||||
odometer - Unadjusted odometer
|
||||
</div>
|
||||
</div>
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -54,7 +84,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -68,7 +98,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -86,10 +116,11 @@
|
||||
initialOdometer - Initial Odometer reading(optional)<br />
|
||||
odometer - Odometer reading<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -103,7 +134,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -122,10 +153,11 @@
|
||||
description - Description<br/>
|
||||
cost - Cost<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -139,7 +171,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -158,10 +190,11 @@
|
||||
description - Description<br />
|
||||
cost - Cost<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -175,7 +208,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -194,10 +227,11 @@
|
||||
description - Description<br />
|
||||
cost - Cost<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -211,7 +245,7 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -229,10 +263,11 @@
|
||||
description - Description<br />
|
||||
cost - Cost<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -250,7 +285,7 @@
|
||||
useUKMPG(bool) - Use UK Imperial Calculation
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
@@ -271,10 +306,11 @@
|
||||
isFillToFull(bool) - Filled To Full<br />
|
||||
missedFuelUp(bool) - Missed Fuel Up<br />
|
||||
notes - notes(optional)<br />
|
||||
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -290,7 +326,7 @@
|
||||
</div>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -305,7 +341,7 @@
|
||||
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -319,7 +355,7 @@
|
||||
No Params(must be root user)
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row api-method">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
@@ -339,4 +375,11 @@
|
||||
$('.copyable').on('click', function (e) {
|
||||
copyToClipboard(e.currentTarget);
|
||||
})
|
||||
function showExtraFieldsInfo(){
|
||||
Swal.fire({
|
||||
title: "Format for Extra Fields",
|
||||
html: "extrafields[i][name] - Name of Extra Field<br/>extrafields[i][value] - Value of Extra Field<br/>i is an integer that starts at 0 and increments with each extrafield",
|
||||
icon: "info"
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
@using CarCareTracker.Helper
|
||||
@{
|
||||
ViewData["Title"] = "Admin";
|
||||
ViewData["Title"] = "Admin Panel";
|
||||
}
|
||||
@inject IConfiguration config;
|
||||
@inject ITranslationHelper translator
|
||||
@@ -25,44 +25,17 @@
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-5 col-12">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
|
||||
<hr />
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate User Token")</button>
|
||||
<div class="col-2 d-flex align-items-center">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Users")</span>
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
|
||||
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Auto Notify(via Email)")</label>
|
||||
</div>
|
||||
<div class="col-10 d-flex align-items-center justify-content-end">
|
||||
<button onclick="showTokenModal()" class="btn btn-primary btn-md mt-1 mb-1">
|
||||
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Manage Tokens")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Token")</th>
|
||||
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Issued To")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (Token token in Model.Tokens)
|
||||
{
|
||||
<tr class="d-flex">
|
||||
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
|
||||
<td class="col-6 text-truncate">@token.EmailAddress</td>
|
||||
<td class="col-2">
|
||||
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Users")</span>
|
||||
<hr />
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
@@ -73,59 +46,120 @@
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (UserData userData in Model.Users)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;">
|
||||
<td class="col-4">@userData.UserName</td>
|
||||
<td class="col-4">@userData.EmailAddress</td>
|
||||
<td class="col-2"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "")/></td>
|
||||
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
|
||||
</tr>
|
||||
}
|
||||
<tbody id="userTable">
|
||||
@await Html.PartialAsync("_Users", Model.Users)
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="tokenModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<div class="btn-group">
|
||||
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1">
|
||||
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate")
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-md mt-1 mb-1" @(emailServerIsSetup ? "" : "disabled") onclick="toggleAutoNotify(event)">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
|
||||
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Notify")</label>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-close btn-md mt-1 mb-1 ms-2" onclick="hideTokenModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Token")</th>
|
||||
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Issued To")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tokenTable">
|
||||
@await Html.PartialAsync("_Tokens", Model.Tokens)
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function updateUserAdmin(userId, sender){
|
||||
function showTokenModal() {
|
||||
$("#tokenModal").modal('show');
|
||||
}
|
||||
function hideTokenModal() {
|
||||
$("#tokenModal").modal('hide');
|
||||
}
|
||||
function reloadTokenTable() {
|
||||
$.get('/Admin/GetTokenPartialView', function (data) {
|
||||
$("#tokenTable").html(data);
|
||||
});
|
||||
}
|
||||
function reloadUserTable() {
|
||||
$.get('/Admin/GetUserPartialView', function (data) {
|
||||
$("#userTable").html(data);
|
||||
});
|
||||
}
|
||||
function updateUserAdmin(userId, sender) {
|
||||
var isChecked = $(sender).is(":checked");
|
||||
$.post('/Admin/UpdateUserAdminStatus', { userId: userId, isAdmin: isChecked }, function (data) {
|
||||
if (data){
|
||||
reloadPage();
|
||||
if (data) {
|
||||
reloadUserTable();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
function reloadPage() {
|
||||
window.location.reload();
|
||||
function toggleAutoNotify(e) {
|
||||
if (!$("#enableAutoNotify").attr("disabled")) {
|
||||
if ($(e.target).hasClass('btn')) {
|
||||
$("#enableAutoNotify").trigger('click');
|
||||
}
|
||||
}
|
||||
}
|
||||
function deleteToken(tokenId) {
|
||||
$.post(`/Admin/DeleteToken?tokenId=${tokenId}`, function (data) {
|
||||
if (data) {
|
||||
reloadPage();
|
||||
reloadTokenTable();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteUser(userId) {
|
||||
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
|
||||
if (data) {
|
||||
reloadPage();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
Swal.fire({
|
||||
title: "Confirm Deletion?",
|
||||
text: "Deleted Users cannot be restored.",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
|
||||
if (data) {
|
||||
reloadUserTable();
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
function generateNewToken() {
|
||||
Swal.fire({
|
||||
title: 'Generate Token',
|
||||
html: `
|
||||
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address">
|
||||
`,
|
||||
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Generate',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
@@ -140,7 +174,7 @@
|
||||
var autoNotify = $("#enableAutoNotify").is(":checked");
|
||||
$.get('/Admin/GenerateNewToken', { emailAddress: result.value.emailAddress, autoNotify: autoNotify }, function (data) {
|
||||
if (data.success) {
|
||||
reloadPage();
|
||||
reloadTokenTable();
|
||||
} else {
|
||||
errorToast(data.message)
|
||||
}
|
||||
|
||||
12
Views/Admin/_Tokens.cshtml
Normal file
12
Views/Admin/_Tokens.cshtml
Normal file
@@ -0,0 +1,12 @@
|
||||
@using CarCareTracker.Helper
|
||||
@model List<Token>
|
||||
@foreach (Token token in Model)
|
||||
{
|
||||
<tr class="d-flex">
|
||||
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
|
||||
<td class="col-6 text-truncate">@StaticHelper.TruncateStrings(token.EmailAddress)</td>
|
||||
<td class="col-2">
|
||||
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
11
Views/Admin/_Users.cshtml
Normal file
11
Views/Admin/_Users.cshtml
Normal file
@@ -0,0 +1,11 @@
|
||||
@using CarCareTracker.Helper
|
||||
@model List<UserData>
|
||||
@foreach (UserData userData in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;">
|
||||
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.UserName)</td>
|
||||
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.EmailAddress)</td>
|
||||
<td class="col-2 d-flex align-items-center"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "") /></td>
|
||||
<td class="col-2 d-flex align-items-center"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
|
||||
</tr>
|
||||
}
|
||||
@@ -9,10 +9,11 @@
|
||||
}
|
||||
@model string
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger";
|
||||
ViewData["Title"] = "Garage";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/garage.js?v=@StaticHelper.VersionNumber"></script>
|
||||
<script src="~/js/settings.js?v=@StaticHelper.VersionNumber"></script>
|
||||
<script src="~/js/supplyrecord.js?v=@StaticHelper.VersionNumber"></script>
|
||||
<script src="~/lib/drawdown/drawdown.js"></script>
|
||||
}
|
||||
@@ -63,31 +64,31 @@
|
||||
<div class="d-flex lubelogger-navbar">
|
||||
<img src="@logoUrl" />
|
||||
<div class="lubelogger-navbar-button">
|
||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>@translator.Translate(userLanguage,"Garage")</button>
|
||||
<button class="nav-link resizable-nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Garage")</span></button>
|
||||
</li>
|
||||
@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" aria-selected="false"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
|
||||
<button class="nav-link resizable-nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
|
||||
</li>
|
||||
}
|
||||
<li 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" aria-selected="false"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</button>
|
||||
<button class="nav-link resizable-nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Calendar")</span></button>
|
||||
</li>
|
||||
<li class="nav-item ms-auto" role="presentation">
|
||||
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</button>
|
||||
<button class="nav-link resizable-nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Settings")</span></button>
|
||||
</li>
|
||||
@if (User.IsInRole("CookieAuth"))
|
||||
{
|
||||
<li class="nav-item dropdown" role="presentation">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person me-2"></i>@User.Identity.Name</a>
|
||||
<a class="nav-link resizable-nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person"></i><span class="ms-2 d-sm-none d-md-inline">@User.Identity.Name</span></a>
|
||||
<ul class="dropdown-menu">
|
||||
@if (User.IsInRole(nameof(UserData.IsAdmin)))
|
||||
{
|
||||
|
||||
@@ -10,15 +10,20 @@
|
||||
<h5 class="modal-title" id="updateAccountModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Account Username")" value="@Model.UserName">
|
||||
<label for="inputEmail">@translator.Translate(userLanguage, "Email Address")</label>
|
||||
<input type="text" id="inputEmail" class="form-control" placeholder="@translator.Translate(userLanguage, "Email Address")" value="@Model.EmailAddress">
|
||||
<label for="inputPassword">@translator.Translate(userLanguage, "New Password")</label>
|
||||
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "New Password")" value="">
|
||||
<label for="inputPassword">@translator.Translate(userLanguage, "New Password")</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "New Password")" value="">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
|
||||
<input type="text" id="inputToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Token")" value="">
|
||||
<div class="row">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
Swal.fire({
|
||||
title: 'Field Name',
|
||||
html: `
|
||||
<input type="text" id="inputExtraFieldName" class="swal2-input" placeholder="Field Name">
|
||||
<input type="text" id="inputExtraFieldName" class="swal2-input" placeholder="Field Name" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Add',
|
||||
focusConfirm: false,
|
||||
|
||||
@@ -30,40 +30,59 @@
|
||||
{
|
||||
@if (!(userConfig.HideSoldVehicles && !string.IsNullOrWhiteSpace(vehicle.SoldDate)))
|
||||
{
|
||||
<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-title="@await Html.PartialAsync("_VehicleExtraFields", vehicle.ExtraFields)" 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-3 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-title="@await Html.PartialAsync("_VehicleExtraFields", vehicle.ExtraFields)" 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; pointer-events:none; @(string.IsNullOrWhiteSpace(vehicle.SoldDate) ? "" : "filter: grayscale(100%);")" />
|
||||
@if (!string.IsNullOrWhiteSpace(vehicle.SoldDate))
|
||||
{
|
||||
<div class="vehicle-sold-banner"><p class='display-6 mb-0'>@translator.Translate(userLanguage, "SOLD")</p></div>
|
||||
} else if (vehicle.LastReportedMileage != default)
|
||||
} else if (vehicle.DashboardMetrics.Any())
|
||||
{
|
||||
<div class="vehicle-sold-banner">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="ms-2"><i class="bi bi-speedometer me-2"></i>@vehicle.LastReportedMileage.ToString("N0")</span>
|
||||
</div>
|
||||
@if (vehicle.HasReminders)
|
||||
{
|
||||
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.Default) && vehicle.LastReportedMileage != default)
|
||||
{
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="me-2"><i class="bi bi bi-bell-fill text-warning"></i></span>
|
||||
<span class="ms-2"><i class="bi bi-speedometer me-2"></i>@vehicle.LastReportedMileage.ToString("N0")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<span></span>
|
||||
@if (vehicle.HasReminders)
|
||||
{
|
||||
<div>
|
||||
<span class="me-2"><i class="bi bi bi-bell-fill text-warning"></i></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.CostPerMile) && vehicle.CostPerMile != default)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.CostPerMile.ToString("C2")}/{vehicle.DistanceUnit}")</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.TotalCost) && vehicle.TotalCost != default)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.TotalCost.ToString("C2")}")</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<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>
|
||||
<h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5>
|
||||
<p class="card-text text-truncate">@vehicle.LicensePlate</p>
|
||||
<p class="card-text text-truncate">@StaticHelper.GetVehicleIdentifier(vehicle)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 garage-item-add">
|
||||
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-4 garage-item-add">
|
||||
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
|
||||
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,18 @@
|
||||
<h5 class="modal-title" id="updateRootAccountModalLabel">@translator.Translate(userLanguage, "Update Profile")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAccountInformationModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Account Username")" value="@Model.UserName">
|
||||
<label for="inputPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "Password")" value="">
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "Password")" value="">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,14 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<input id="preferredGasUnit" style="display:none;" value="@Model.UserConfig.PreferredGasUnit" />
|
||||
<input id="preferredFuelMileageUnit" style="display:none;" value="@Model.UserConfig.PreferredGasMileageUnit" />
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
|
||||
<label class="form-check-label" for="enableDarkMode">@translator.Translate(userLanguage, "Dark Mode")</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="useSystemColorMode" checked="@Model.UserConfig.UseSystemColorMode">
|
||||
<label class="form-check-label" for="useSystemColorMode">@translator.Translate(userLanguage, "Adaptive Color Mode")</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableCsvImports" checked="@Model.UserConfig.EnableCsvImports">
|
||||
<label class="form-check-label" for="enableCsvImports">@translator.Translate(userLanguage, "Enable CSV Imports")</label>
|
||||
@@ -67,16 +71,34 @@
|
||||
</div>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.UserConfig.EnableAuth">
|
||||
<label class="form-check-label" for="enableAuth">@translator.Translate(userLanguage, "Enable Authentication")</label>
|
||||
</div>
|
||||
@if (Model.UserConfig.EnableAuth)
|
||||
{
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="disableRegistration" checked="@Model.UserConfig.DisableRegistration">
|
||||
<label class="form-check-label" for="disableRegistration">@translator.Translate(userLanguage, "Disable Registration")</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableRootUserOIDC" checked="@Model.UserConfig.EnableRootUserOIDC">
|
||||
<label class="form-check-label" for="enableRootUserOIDC">@translator.Translate(userLanguage, "Enable OIDC for Root User")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Uses the Default Reminder Email for OIDC Auth")</small></label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row" id="visibleTabs">
|
||||
<div class="col-12">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Visible Tabs")</span>
|
||||
<div class="row">
|
||||
<div class="col-11">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Visible Tabs")</span>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button onclick="showTabReorderModal()" class="btn text-secondary btn-sm"><i class="bi bi-arrow-down-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="list-group">
|
||||
@@ -150,12 +172,28 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
|
||||
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
|
||||
@foreach (string uiLanguage in Model.UILanguages)
|
||||
{
|
||||
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
|
||||
}
|
||||
</select>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
<div class="input-group">
|
||||
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
|
||||
@foreach (string uiLanguage in Model.UILanguages)
|
||||
{
|
||||
<!option value="@uiLanguage" @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@StaticHelper.GetTranslationName(uiLanguage)</!option>
|
||||
}
|
||||
</select>
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="showTranslationEditor()"><i class="bi bi-pencil"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
} else
|
||||
{
|
||||
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
|
||||
@foreach (string uiLanguage in Model.UILanguages)
|
||||
{
|
||||
<!option value="@uiLanguage" @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@StaticHelper.GetTranslationName(uiLanguage)</!option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
@@ -165,11 +203,11 @@
|
||||
<span class="lead">@translator.Translate(userLanguage, "Backups")</span>
|
||||
<div class="row">
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="makeBackup()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Create")</button>
|
||||
<button onclick="makeBackup()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Create")</button>
|
||||
</div>
|
||||
<div class="col-6 d-grid">
|
||||
<input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup">
|
||||
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">@translator.Translate(userLanguage, "Restore")</button>
|
||||
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md text-truncate">@translator.Translate(userLanguage, "Restore")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,23 +216,45 @@
|
||||
<div class="row">
|
||||
<div class="col-6 d-grid">
|
||||
<input onChange="uploadLanguage(this)" type="file" accept=".json" class="d-none" id="inputLanguage">
|
||||
<button onclick="openUploadLanguage()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Upload")</button>
|
||||
<div class="btn-group">
|
||||
<button onclick="openUploadLanguage()" class="btn btn-primary btn-md text-truncate"><i class="bi bi-upload"></i><span class="ms-2 d-sm-inline d-md-none d-xl-inline">@translator.Translate(userLanguage, "Upload")</span></button>
|
||||
<button type="button" class="btn btn-md btn-primary btn-md 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="showTranslationDownloader()">@translator.Translate(userLanguage, "Get Translations")</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="deleteLanguage()" @(Model.UserConfig.UserLanguage == "en_US" ? "disabled" : "") class="btn btn-danger btn-md">@translator.Translate(userLanguage, "Delete")</button>
|
||||
<button onclick="deleteLanguage()" @(Model.UserConfig.UserLanguage == "en_US" ? "disabled" : "") class="btn btn-danger btn-md text-truncate"><i class="bi bi-trash"></i><span class="ms-2 d-sm-inline d-md-none d-xl-inline">@translator.Translate(userLanguage, "Delete")</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead">@translator.Translate(userLanguage, "Server-wide Settings")</span>
|
||||
<span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span>
|
||||
<div class="row">
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Extra Fields")</button>
|
||||
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button>
|
||||
</div>
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="showReminderUrgencyThresholdModal()" class="btn btn-primary btn-md">@translator.Translate(userLanguage, "Reminders")</button>
|
||||
<button onclick="showReminderUrgencyThresholdModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Reminders")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead text-wrap">@translator.Translate(userLanguage, "Default Reminder Email")</span>
|
||||
<div class="row">
|
||||
<div class="col-12 d-grid">
|
||||
<div class="input-group">
|
||||
<input id="inputDefaultEmail" class="form-control" placeholder="@translator.Translate(userLanguage,"Default Email for Reminder")" value="@Model.UserConfig.DefaultReminderEmail" onkeydown="handleDefaultReminderInputKeyDown()">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="updateSettings()"><i class="bi bi-floppy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,13 +311,59 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@await Html.PartialAsync("_Sponsors", Model.Sponsors)
|
||||
<div class="row" id="sponsorsContainer"></div>
|
||||
<div class="modal fade" data-bs-focus="false" id="extraFieldModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="extraFieldModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="translationEditorModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="translationEditorModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="translationDownloadModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="translationDownloadModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="tabReorderModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="tabReorderModalContent">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="translationEditorModalLabel">@translator.Translate(userLanguage, "Reorder Tabs")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideTabReorderModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ul class="list-group lubelog-tab-groups">
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)" draggable="true" data-tab="@ImportMode.Dashboard">@translator.Translate(userLanguage, "Dashboard")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)" draggable="true" data-tab="@ImportMode.PlanRecord">@translator.Translate(userLanguage, "Planner")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)" draggable="true" data-tab="@ImportMode.OdometerRecord">@translator.Translate(userLanguage, "Odometer")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)" draggable="true" data-tab="@ImportMode.ServiceRecord">@translator.Translate(userLanguage, "Service Records")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)" draggable="true" data-tab="@ImportMode.RepairRecord">@translator.Translate(userLanguage, "Repairs")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)" draggable="true" data-tab="@ImportMode.UpgradeRecord">@translator.Translate(userLanguage, "Upgrades")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)" draggable="true" data-tab="@ImportMode.GasRecord">@translator.Translate(userLanguage, "Fuel")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)" draggable="true" data-tab="@ImportMode.SupplyRecord">@translator.Translate(userLanguage, "Supplies")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)" draggable="true" data-tab="@ImportMode.TaxRecord">@translator.Translate(userLanguage, "Taxes")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)" draggable="true" data-tab="@ImportMode.NoteRecord">@translator.Translate(userLanguage, "Notes")</li>
|
||||
<li class="list-group-item" style="order: @Model.UserConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)" draggable="true" data-tab="@ImportMode.ReminderRecord">@translator.Translate(userLanguage, "Reminder")</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger me-auto" onclick="resetTabOrder()">@translator.Translate(userLanguage, "Reset Tab Order")</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="hideTabReorderModal()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
<button type="button" class="btn btn-primary" onclick="updateSettings()">@translator.Translate(userLanguage, "Save Tab Order")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function showReminderUrgencyThresholdModal(){
|
||||
Swal.fire({
|
||||
@@ -306,137 +412,6 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
function showExtraFieldModal() {
|
||||
$.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) {
|
||||
$("#extraFieldModalContent").html(data);
|
||||
$("#extraFieldModal").modal('show');
|
||||
});
|
||||
}
|
||||
function hideExtraFieldModal() {
|
||||
$("#extraFieldModal").modal('hide');
|
||||
}
|
||||
function getCheckedTabs() {
|
||||
var visibleTabs = $("#visibleTabs :checked").map(function () {
|
||||
return this.value;
|
||||
});
|
||||
return visibleTabs.toArray();
|
||||
}
|
||||
function deleteLanguage() {
|
||||
var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`;
|
||||
$.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) {
|
||||
//reset user language back to en_US
|
||||
$("#defaultLanguage").val('en_US');
|
||||
updateSettings();
|
||||
});
|
||||
}
|
||||
function updateSettings() {
|
||||
var visibleTabs = getCheckedTabs();
|
||||
var defaultTab = $("#defaultTab").val();
|
||||
if (!visibleTabs.includes(defaultTab)) {
|
||||
defaultTab = "Dashboard"; //default to dashboard.
|
||||
}
|
||||
var userConfigObject = {
|
||||
useDarkMode: $("#enableDarkMode").is(':checked'),
|
||||
enableCsvImports: $("#enableCsvImports").is(':checked'),
|
||||
useMPG: $("#useMPG").is(':checked'),
|
||||
useDescending: $("#useDescending").is(':checked'),
|
||||
hideZero: $("#hideZero").is(":checked"),
|
||||
useUKMpg: $("#useUKMPG").is(":checked"),
|
||||
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
|
||||
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
|
||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
||||
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
|
||||
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
|
||||
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
|
||||
preferredGasUnit: $("#preferredGasUnit").val(),
|
||||
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
|
||||
userLanguage: $("#defaultLanguage").val(),
|
||||
visibleTabs: visibleTabs,
|
||||
defaultTab: defaultTab
|
||||
}
|
||||
sloader.show();
|
||||
$.post('/Home/WriteToSettings', { userConfig: userConfigObject }, function (data) {
|
||||
sloader.hide();
|
||||
if (data) {
|
||||
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
})
|
||||
}
|
||||
function makeBackup() {
|
||||
$.get('/Files/MakeBackup', function (data) {
|
||||
window.location.href = data;
|
||||
});
|
||||
}
|
||||
function openUploadLanguage() {
|
||||
$("#inputLanguage").click();
|
||||
}
|
||||
function openRestoreBackup() {
|
||||
$("#inputBackup").click();
|
||||
}
|
||||
function uploadLanguage(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", event.files[0]);
|
||||
sloader.show();
|
||||
$.ajax({
|
||||
url: "/Files/HandleTranslationFileUpload",
|
||||
data: formData,
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
sloader.hide();
|
||||
if (response.success) {
|
||||
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
|
||||
} else {
|
||||
errorToast(response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
sloader.hide();
|
||||
errorToast("An error has occurred, please check the file size and try again later.");
|
||||
}
|
||||
});
|
||||
}
|
||||
function restoreBackup(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", event.files[0]);
|
||||
console.log('LubeLogger - DB Restoration Started');
|
||||
sloader.show();
|
||||
$.ajax({
|
||||
url: "/Files/HandleFileUpload",
|
||||
data: formData,
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.trim() != '') {
|
||||
$.post('/Files/RestoreBackup', { fileName: response }, function (data) {
|
||||
sloader.hide();
|
||||
if (data) {
|
||||
console.log('LubeLogger - DB Restoration Completed');
|
||||
successToast("Backup Restored");
|
||||
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
console.log('LubeLogger - DB Restoration Failed - Failed to process backup file.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('LubeLogger - DB Restoration Failed - Failed to upload backup file.');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
sloader.hide();
|
||||
console.log('LubeLogger - DB Restoration Failed - Request failed to reach backend, please check file size.');
|
||||
errorToast("An error has occurred, please check the file size and try again later.");
|
||||
}
|
||||
});
|
||||
}
|
||||
function enableAuthCheckChanged() {
|
||||
var enableAuth = $("#enableAuth").is(":checked");
|
||||
if (enableAuth) {
|
||||
@@ -445,7 +420,7 @@
|
||||
title: 'Setup Credentials',
|
||||
html: `
|
||||
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
|
||||
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
|
||||
<input type="password" id="authPassword" class="swal2-input" placeholder="Password" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Setup',
|
||||
focusConfirm: false,
|
||||
@@ -478,4 +453,5 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
loadSponsors();
|
||||
</script>
|
||||
@@ -7,14 +7,13 @@
|
||||
var enableAuth = userConfig.EnableAuth;
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center">
|
||||
<h6 class="display-6 mt-2">@translator.Translate(userLanguage, "Sponsors")</h6>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-center">
|
||||
<p><a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://docs.lubelogger.com/Funding" target="_blank">Become a Sponsor</a></p>
|
||||
<p><a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://docs.lubelogger.com/Misc/Funding" target="_blank">Become a Sponsor</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.LifeTime.Any())
|
||||
@@ -69,5 +68,4 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
39
Views/Home/_TranslationEditor.cshtml
Normal file
39
Views/Home/_TranslationEditor.cshtml
Normal file
@@ -0,0 +1,39 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model Dictionary<string, string>
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="translationEditorModalLabel">@translator.Translate(userLanguage, "Translation Editor")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideTranslationEditor()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form class="form-inline">
|
||||
<div class="form-group" style="max-height:50vh; overflow-x:hidden; overflow-y:scroll;">
|
||||
@foreach(var translation in Model)
|
||||
{
|
||||
<div class="row translation-keyvalue mb-2">
|
||||
<div class="col-md-6 col-12 translation-key">@translation.Key.Replace("_", " ")</div>
|
||||
<div class="col-md-6 col-12 translation-value">
|
||||
<textarea style="height:100%;width:98%;" class="form-control" placeholder="@translation.Value">@translation.Value</textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideTranslationEditor()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="saveTranslation()" class="btn btn-primary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Save Translation")</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="exportTranslation()">@translator.Translate(userLanguage, "Export Translation")</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
76
Views/Home/_Translations.cshtml
Normal file
76
Views/Home/_Translations.cshtml
Normal file
@@ -0,0 +1,76 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model Translations
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="translationDownloaderModalLabel">@translator.Translate(userLanguage, "Available Translations")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideTranslationDownloader()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form class="form-inline">
|
||||
<div class="form-group" style="max-height:50vh; overflow-x:hidden; overflow-y:scroll;">
|
||||
@foreach(var translation in Model.Africa)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Africa','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var translation in Model.Asia)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Asia','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var translation in Model.Europe)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Europe','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var translation in Model.NorthAmerica)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('NorthAmerica','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var translation in Model.SouthAmerica)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('SouthAmerica','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var translation in Model.Oceania)
|
||||
{
|
||||
<div class="row mb-2">
|
||||
<div class="col-10">@StaticHelper.GetTranslationName(translation)</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-primary" onclick="downloadTranslation('Oceania','@translation')"><i class="bi bi-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideTranslationDownloader()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
<button type="button" class="btn btn-primary" onclick="downloadAllTranslations()">@translator.Translate(userLanguage, "Download All Translations")</button>
|
||||
</div>
|
||||
@@ -6,14 +6,14 @@
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Login";
|
||||
ViewData["Title"] = "Forgot Password";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<div style="max-width:204px;">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
|
||||
|
||||
@@ -5,25 +5,31 @@
|
||||
@{
|
||||
var logoUrl = config.GetLogoUrl();
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
var registrationDisabled = config.GetServerDisabledRegistration();
|
||||
var openIdConfigName = config.GetOpenIDConfig().Name;
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Login";
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<div style="max-width:204px;">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>
|
||||
<input type="text" id="inputUserName" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="inputPersistent">
|
||||
@@ -46,9 +52,12 @@
|
||||
<div class="d-grid">
|
||||
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
|
||||
</div>
|
||||
@if (!registrationDisabled)
|
||||
{
|
||||
<div class="d-grid">
|
||||
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
}
|
||||
@model string
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Register";
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<div style="max-width:204px;">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Register";
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<div style="max-width:204px;">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
|
||||
@@ -29,7 +29,12 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "Password")</label>
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Register")</button>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
var userLanguage = config.GetServerLanguage();
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Register";
|
||||
ViewData["Title"] = "Reset Password";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js?v=@StaticHelper.VersionNumber"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<div style="max-width:204px;">
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
|
||||
@@ -25,7 +25,12 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label>
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<div class="input-group">
|
||||
<input type="password" id="inputUserPassword" class="form-control">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Reset Password")</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@using CarCareTracker.Helper
|
||||
@{
|
||||
ViewData["Title"] = "Admin";
|
||||
ViewData["Title"] = "Database Migration";
|
||||
}
|
||||
@inject IConfiguration config;
|
||||
@inject ITranslationHelper translator
|
||||
@@ -83,6 +83,6 @@
|
||||
});
|
||||
}
|
||||
function importToPostgres() {
|
||||
$("#inputImport").click();
|
||||
$("#inputImport").trigger('click');
|
||||
}
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var useDarkMode = userConfig.UseDarkMode;
|
||||
var useSystemColorMode = userConfig.UseSystemColorMode;
|
||||
var enableCsvImports = userConfig.EnableCsvImports;
|
||||
var useMPG = userConfig.UseMPG;
|
||||
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
|
||||
@@ -30,7 +31,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="LubeLogger" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>@ViewData["Title"] - CarCareTracker</title>
|
||||
<title>@ViewData["Title"] - LubeLogger</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
|
||||
@@ -54,7 +55,7 @@
|
||||
<script>
|
||||
function getGlobalConfig() {
|
||||
return {
|
||||
useDarkMode : "@useDarkMode" == "True",
|
||||
useDarkMode: "@useDarkMode" == "True" || ("@useSystemColorMode" == "True" && window.matchMedia('(prefers-color-scheme: dark)').matches),
|
||||
enableCsvImport : "@enableCsvImports" == "True",
|
||||
useMarkDown: "@useMarkDown" == "True",
|
||||
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
|
||||
@@ -111,6 +112,12 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
function setThemeBasedOnDevice() {
|
||||
var systemPrefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (systemPrefersDarkMode) {
|
||||
$(document.documentElement).attr("data-bs-theme", "dark");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</head>
|
||||
@@ -122,3 +129,9 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@if (useSystemColorMode)
|
||||
{
|
||||
<script>
|
||||
setThemeBasedOnDevice();
|
||||
</script>
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
@using CarCareTracker.Helper
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - View Vehicle";
|
||||
}
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@{
|
||||
@@ -9,6 +6,9 @@
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
@model Vehicle
|
||||
@{
|
||||
ViewData["Title"] = $"{Model.Year} {Model.Make} {Model.Model} ({StaticHelper.GetVehicleIdentifier(Model)})";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/vehicle.js?v=@StaticHelper.VersionNumber"></script>
|
||||
<script src="~/js/servicerecord.js?v=@StaticHelper.VersionNumber"></script>
|
||||
@@ -27,101 +27,92 @@
|
||||
}
|
||||
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
||||
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: -2">
|
||||
<button class="nav-link" onclick="returnToGarage()"><span class="display-3 ms-2"><i class="bi bi-arrow-left-square me-2"></i>@translator.Translate(userLanguage,"Garage")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: -1">
|
||||
<button class="nav-link" onclick="editVehicle(@Model.Id)"><span class="display-3 ms-2"><i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Edit Vehicle")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>@translator.Translate(userLanguage, "Dashboard")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-bar-chart-steps me-2"></i>@translator.Translate(userLanguage, "Planner")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-speedometer me-2"></i>@translator.Translate(userLanguage, "Odometer")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>@translator.Translate(userLanguage, "Service Records")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-exclamation-octagon me-2"></i>@translator.Translate(userLanguage,"Repairs")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-wrench-adjustable me-2"></i>@translator.Translate(userLanguage, "Upgrades")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Fuel")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-currency-dollar me-2"></i>@translator.Translate(userLanguage, "Taxes")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-journal-bookmark me-2"></i>@translator.Translate(userLanguage, "Notes")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>@translator.Translate(userLanguage, "Reminders")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><span class="display-3 ms-2"><i class="bi bi-trash me-2"></i>@translator.Translate(userLanguage, "Delete Vehicle")</span></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<button onclick="returnToGarage()" class="lubelogger-tab btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
|
||||
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{Model.LicensePlate})")</small></h1>
|
||||
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{StaticHelper.GetVehicleIdentifier(Model)})")</small></h1>
|
||||
<button onclick="editVehicle(@Model.Id)" class="lubelogger-tab btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
|
||||
<div class="lubelogger-navbar-button">
|
||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>@translator.Translate(userLanguage,"Dashboard")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Dashboard")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps me-2"></i>@translator.Translate(userLanguage, "Planner")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Planner")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer me-2"></i>@translator.Translate(userLanguage, "Odometer")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Odometer")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>@translator.Translate(userLanguage, "Service Records")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Service Records")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>@translator.Translate(userLanguage, "Repairs")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Repairs")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>@translator.Translate(userLanguage, "Upgrades")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Upgrades")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>@translator.Translate(userLanguage, "Fuel")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Fuel")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>@translator.Translate(userLanguage, "Taxes")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Taxes")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>@translator.Translate(userLanguage, "Notes")</button>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Notes")</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>@translator.Translate(userLanguage, "Reminders")</button>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-auto" role="presentation">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">@translator.Translate(userLanguage, "Manage Vehicle")</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><i class="bi bi-trash me-2"></i>@translator.Translate(userLanguage, "Delete Vehicle")</button></li>
|
||||
</ul>
|
||||
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)">
|
||||
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell"></i></div><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Reminders")</span></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="vehicleTabContent">
|
||||
@@ -138,7 +129,7 @@
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.OdometerRecord)" id="odometer-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="editVehicleModal" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" data-bs-focus="false" id="editVehicleModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="editVehicleModalContent">
|
||||
</div>
|
||||
@@ -171,7 +162,9 @@
|
||||
function GetVehicleId() {
|
||||
return {
|
||||
vehicleId: @Model.Id,
|
||||
odometerOptional: @Model.OdometerOptional.ToString().ToLower(),
|
||||
hasOdometerAdjustment: @Model.HasOdometerAdjustment.ToString().ToLower(),
|
||||
useEngineHours: @Model.UseHours.ToString().ToLower(),
|
||||
odometerDifference: decodeHTMLEntities('@Model.OdometerDifference'),
|
||||
odometerMultiplier: decodeHTMLEntities('@Model.OdometerMultiplier')
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
Swal.fire({
|
||||
title: 'Add Collaborator',
|
||||
html: `
|
||||
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username">
|
||||
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Add',
|
||||
focusConfirm: false,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Repair Record") : translator.Translate(userLanguage, "Edit Repair Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditCollisionRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddCollisionRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<label for="collisionRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
|
||||
<div class="input-group">
|
||||
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
|
||||
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew || Model.Mileage == default ? "" : Model.Mileage)">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="input-group-text">
|
||||
|
||||
@@ -107,14 +107,14 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2 col-xl-1 flex-grow-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
|
||||
<th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1" data-column="description">@translator.Translate(userLanguage, "Description")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
<th scope="col" class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date">@translator.Translate(userLanguage, "Date")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
|
||||
<th scope="col" class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@translator.Translate(userLanguage, "Description")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -122,10 +122,10 @@
|
||||
@foreach (CollisionRecord collisionRecord in Model)
|
||||
{
|
||||
<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 flex-grow-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(collisionRecord.Date)">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@collisionRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1" data-column="description">@collisionRecord.Description</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(collisionRecord.Date)">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@(collisionRecord.Mileage == default ? "---" : collisionRecord.Mileage.ToString())</td>
|
||||
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1 text-truncate" data-column="description">@collisionRecord.Description</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
]
|
||||
},
|
||||
options: {
|
||||
onClick: (e) => {
|
||||
showDataTable();
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
|
||||
78
Views/Vehicle/_CostTableReport.cshtml
Normal file
78
Views/Vehicle/_CostTableReport.cshtml
Normal file
@@ -0,0 +1,78 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
var hideZero = userConfig.HideZero;
|
||||
}
|
||||
@model CostTableForVehicle
|
||||
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
|
||||
{
|
||||
<div>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(translator.Translate(userLanguage, "Vehicle Cost Breakdown"))</h5>
|
||||
<button type="button" class="btn-close" onclick="hideDataTable()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Type")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Cost Per Day")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, Model.DistanceUnit)</th>
|
||||
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Service Records")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordPerDay == default ? "---" : Model.ServiceRecordPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordPerMile == default ? "---" :Model.ServiceRecordPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordSum == default ? "---" :Model.ServiceRecordSum.ToString("C2"))</td>
|
||||
</tr>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Repairs")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordPerDay == default ? "---" :Model.CollisionRecordPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordPerMile == default ? "---" :Model.CollisionRecordPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordSum == default ? "---" :Model.CollisionRecordSum.ToString("C2"))</td>
|
||||
</tr>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Upgrades")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordPerDay == default ? "---" :Model.UpgradeRecordPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordPerMile == default ? "---" :Model.UpgradeRecordPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordSum == default ? "---" :Model.UpgradeRecordSum.ToString("C2"))</td>
|
||||
</tr>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Fuel")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordPerDay == default ? "---" :Model.GasRecordPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordPerMile == default ? "---" :Model.GasRecordPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordSum == default ? "---" :Model.GasRecordSum.ToString("C2"))</td>
|
||||
</tr>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Taxes")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordPerDay == default ? "---" :Model.TaxRecordPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordPerMile == default ? "---" : Model.TaxRecordPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordSum == default ? "---" :Model.TaxRecordSum.ToString("C2"))</td>
|
||||
</tr>
|
||||
<tr class="d-flex">
|
||||
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalPerDay == default ? "---" : Model.TotalPerDay.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalPerMile == default ? "---" : Model.TotalPerMile.ToString("C2"))</td>
|
||||
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalCost == default ? "---" : Model.TotalCost.ToString("C2"))</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center">
|
||||
<h4>@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")</h4>
|
||||
</div>
|
||||
}
|
||||
@@ -171,17 +171,17 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2 flex-grow-1" data-column="daterefueled">@translator.Translate(userLanguage, "Date Refueled")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1" data-column="delta" style="cursor:pointer;" onclick="toggleSort('gas-tab-pane', this)">@($"Δ({distanceUnit})")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="consumption" data-gas="consumption" data-unit="@consumptionUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{translator.Translate(userLanguage, "Consumption")}({consumptionUnit})")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="fueleconomy" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1" data-column="cost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1" data-column="unitcost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Unit Cost")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" style='display:none;' data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 text-truncate" data-column="daterefueled">@translator.Translate(userLanguage, "Date Refueled")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="delta" style="cursor:pointer;" onclick="toggleSort('gas-tab-pane', this)">@($"Δ({distanceUnit})")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="consumption" data-gas="consumption" data-unit="@consumptionUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{translator.Translate(userLanguage, "Consumption")}({consumptionUnit})")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Cost")</th>
|
||||
<th scope="col" class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Unit Cost")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -189,13 +189,13 @@
|
||||
@foreach (GasRecordViewModel gasRecord in Model.GasRecords)
|
||||
{
|
||||
<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 flex-grow-1" data-column="daterefueled">@gasRecord.Date</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@gasRecord.Mileage</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1" data-column="delta">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="consumption" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1" data-column="unitcost" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
|
||||
<td class="col-2 flex-grow-1 text-truncate" data-column="daterefueled">@gasRecord.Date</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@(gasRecord.Mileage == default ? "---" : gasRecord.Mileage.ToString())</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="delta">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="consumption" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="cost" data-record-type="cost">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
|
||||
<td class="col-1 flex-grow-1 flex-shrink-1 text-truncate" data-column="unitcost" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" style='display:none;' data-column="notes">@StaticHelper.TruncateStrings(gasRecord.Notes)</td>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
|
||||
var barGraphColors = StaticHelper.GetBarChartColors();
|
||||
var sortedByMPG = Model.OrderBy(x => x.Cost).ToList();
|
||||
}
|
||||
@if (Model.Where(x=>x.Cost > 0).Any() || Model.Where(x=>x.DistanceTraveled > 0).Any())
|
||||
@if (Model.Any(x=>x.Cost > 0) || Model.Any(x=>x.DistanceTraveled > 0))
|
||||
{
|
||||
<canvas id="bar-chart"></canvas>
|
||||
<script>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Gas Record") : translator.Translate(userLanguage, "Edit Gas Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditGasRecordModal({Model.GasRecord.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<label for="gasRecordMileage">@($"{translator.Translate(userLanguage,"Odometer Reading")}({distanceUnit})")</label>
|
||||
<div class="input-group">
|
||||
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
|
||||
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew || Model.GasRecord.Mileage == default ? "" : Model.GasRecord.Mileage)">
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="input-group-text">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h5 class="modal-title">@translator.Translate(userLanguage,"Edit Multiple Gas Records")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -28,6 +28,14 @@
|
||||
<input type="text" inputmode="decimal" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<label for="gasRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||
<select multiple class="form-select" id="gasRecordTag"></select>
|
||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<div class="extra-field">
|
||||
<label for="@elementId">@field.Name</label>
|
||||
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="gasRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<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">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -28,6 +28,14 @@
|
||||
<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>
|
||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<div class="extra-field">
|
||||
<label for="@elementId">@field.Name</label>
|
||||
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
</div>
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model List<CostForVehicleByMonth>
|
||||
@model MPGForVehicleByMonth
|
||||
@{
|
||||
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
|
||||
var sortedByMPG = Model.OrderByDescending(x => x.Cost).ToList();
|
||||
var barGraphColors = StaticHelper.GetBarChartColors();
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
@if (Model.Where(x => x.Cost > 0).Any())
|
||||
@if (Model.CostData.Any(x => x.Cost > 0))
|
||||
{
|
||||
|
||||
<canvas id="bar-chart-mpg"></canvas>
|
||||
@@ -20,11 +19,11 @@
|
||||
//color gradient from high to low
|
||||
var barGraphColors = [];
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||
@foreach (CostForVehicleByMonth gasCost in Model.CostData)
|
||||
{
|
||||
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||
var index = Model.SortedCostData.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||
@:barGraphColors.push('@barGraphColors[index]');
|
||||
}
|
||||
new Chart($("#bar-chart-mpg"), {
|
||||
@@ -44,7 +43,7 @@
|
||||
title: {
|
||||
display: true,
|
||||
color: useDarkMode ? "#fff" : "#000",
|
||||
text: decodeHTMLEntities('@translator.Translate(userLanguage, "Fuel Mileage by Month")')
|
||||
text: decodeHTMLEntities('@($"{translator.Translate(userLanguage, "Fuel Mileage by Month")}({Model.Unit})")')
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Note") : translator.Translate(userLanguage, "Edit Note"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditNoteModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddNoteModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
|
||||
@@ -11,7 +11,27 @@
|
||||
<h5 class="modal-title">@(isNew ? translator.Translate(userLanguage, "Add New Odometer Record") : translator.Translate(userLanguage, "Edit Odometer Record"))<small style="display:none; @(isNew ? "" : "cursor:pointer;")" class="cached-banner ms-2 text-warning" onclick='@(isNew ? "" : $"showEditOdometerRecordModal({Model.Id}, true)" )'>@translator.Translate(userLanguage, "Unsaved Changes")</small></h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddOdometerRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body d-none trip-modal">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning alert-dismissable" role="alert">
|
||||
@translator.Translate(userLanguage, "Experimental Feature - Do not exit or minimize this app when recording. Verify all starting and ending odometers. Accuracy subject to hardware limitations.")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h1 class="display-6 text-body-secondary">@translator.Translate(userLanguage, "Current Odometer")</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center align-items-center">
|
||||
<h1 class="display-1 trip-odometer" onclick="toggleSubOdometer()"></h1>
|
||||
<h1 class="display-6 trip-odometer-sub ms-2 d-none">0</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body odometer-modal" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -40,6 +60,9 @@
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="getLastOdometerReadingAndIncrement('odometerRecordMileage')"><i class="bi bi-plus"></i></button>
|
||||
</div>
|
||||
<div class="input-group-text trip-show d-none">
|
||||
<button type="button" class="btn btn-sm btn-danger zero-y-padding" onclick="showTripModal()"><i class="bi bi-record-fill"></i></button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<label for="odometerRecordTag">@translator.Translate(userLanguage,"Tags(optional)")</label>
|
||||
@@ -82,7 +105,13 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer d-none trip-modal">
|
||||
<button type="button" class="btn btn-danger trip-start" onclick="startRecording()" style="margin-right:auto;">@translator.Translate(userLanguage, "Start Recording")</button>
|
||||
<button type="button" class="btn btn-danger d-none trip-stop" onclick="stopRecording()" style="margin-right:auto;">@translator.Translate(userLanguage, "Stop Recording")</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="hideTripModal()">@translator.Translate(userLanguage, "Cancel")</button>
|
||||
<button type="button" class="btn btn-primary d-none trip-save" onclick="saveRecordedOdometer()">@translator.Translate(userLanguage, "Save")</button>
|
||||
</div>
|
||||
<div class="modal-footer odometer-modal">
|
||||
@if (!isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-danger" onclick="deleteOdometerRecord(@Model.Id)" style="margin-right:auto;">@translator.Translate(userLanguage,"Delete")</button>
|
||||
@@ -98,6 +127,11 @@
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
//Trip Recording Variables
|
||||
var tripTimer = undefined; //interval to check GPS Location every 5 seconds.
|
||||
var tripWakeLock = undefined; //wakelock handler to prevent screen from going to sleep.
|
||||
var tripLastPosition = undefined; //last coordinates to compare/calculate distance from.
|
||||
var tripCoordinates = ["Latitude,Longitude"]; //list of coordinates to generate a CSV for.
|
||||
var uploadedFiles = [];
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@@ -109,4 +143,5 @@
|
||||
function getOdometerRecordModelData() {
|
||||
return { id: @Model.Id}
|
||||
}
|
||||
checkTripRecorder();
|
||||
</script>
|
||||
@@ -107,14 +107,14 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2 col-xl-1 flex-grow-1" data-column="date">@translator.Translate(userLanguage, "Date")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="initialodometer">@translator.Translate(userLanguage, "Initial Odometer")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1" data-column="distance" onclick="toggleSort('odometer-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Distance")</th>
|
||||
<th scope="col" class="col-2 col-xl-3 flex-grow-1 flex-shrink-1" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
<th scope="col" class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date">@translator.Translate(userLanguage, "Date")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@translator.Translate(userLanguage, "Initial Odometer")</th>
|
||||
<th scope="col" class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@translator.Translate(userLanguage, "Odometer")</th>
|
||||
<th scope="col" class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" onclick="toggleSort('odometer-tab-pane', this)" style="cursor:pointer;">@translator.Translate(userLanguage, "Distance")</th>
|
||||
<th scope="col" class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@translator.Translate(userLanguage, "Notes")</th>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
<th scope="col" style='display:none;' class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="@extraFieldColumn">@extraFieldColumn</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -122,10 +122,10 @@
|
||||
@foreach (OdometerRecord odometerRecord in Model)
|
||||
{
|
||||
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@odometerRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditOdometerRecordModal,@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
|
||||
<td class="col-2 col-xl-1 flex-grow-1" data-column="date">@odometerRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1" data-column="initialodometer">@odometerRecord.InitialMileage</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1" data-column="odometer">@odometerRecord.Mileage</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="distance" data-record-type="distance">@(odometerRecord.DistanceTraveled == default ? "---" : odometerRecord.DistanceTraveled)</td>
|
||||
<td class="col-2 col-xl-1 flex-grow-1 text-truncate" data-column="date">@odometerRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="initialodometer">@odometerRecord.InitialMileage</td>
|
||||
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="odometer">@odometerRecord.Mileage</td>
|
||||
<td class="col-2 flex-grow-1 flex-shrink-1 text-truncate" data-column="distance" data-record-type="distance">@(odometerRecord.DistanceTraveled == default ? "---" : odometerRecord.DistanceTraveled)</td>
|
||||
<td class="col-2 col-xl-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
|
||||
@foreach (string extraFieldColumn in extraFields)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h5 class="modal-title">@translator.Translate(userLanguage,"Edit Multiple Odometer Records")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddOdometerRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" onkeydown="handleEnter(this)">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -26,6 +26,14 @@
|
||||
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
<label for="odometerRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
|
||||
<select multiple class="form-select" id="odometerRecordTag"></select>
|
||||
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
|
||||
{
|
||||
var elementId = Guid.NewGuid();
|
||||
<div class="extra-field">
|
||||
<label for="@elementId">@field.Name</label>
|
||||
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="odometerRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user