Compare commits

...

77 Commits

Author SHA1 Message Date
Hargata Softworks
4a007530a6 Merge pull request #188 from hargata/Hargata/parse.url
added setting to automatically load parsed markdown.
2024-01-30 08:19:04 -07:00
DESKTOP-GENO133\IvanPlex
04ce448b23 removed onclick event since it ain;t helping. 2024-01-30 08:18:45 -07:00
DESKTOP-GENO133\IvanPlex
ccd446f299 added setting to automatically load parsed markdown. 2024-01-30 08:16:28 -07:00
Hargata Softworks
c49c8a5301 Merge pull request #186 from hargata/Hargata/parse.url
Hargata/parse.url
2024-01-29 21:05:24 -07:00
DESKTOP-GENO133\IvanPlex
55cc2819d0 show overlay in place instead. 2024-01-29 21:04:22 -07:00
DESKTOP-GENO133\IvanPlex
f8de7de0d6 added markdown for named notes. 2024-01-29 20:33:37 -07:00
Hargata Softworks
2ea1bc2c20 Merge pull request #185 from hargata/Hargata/parse.url
Added Markdown Parsing.
2024-01-29 19:53:38 -07:00
DESKTOP-GENO133\IvanPlex
90d095ea51 Updated version. 2024-01-29 19:50:01 -07:00
DESKTOP-GENO133\IvanPlex
e1d12d0918 added dependency 2024-01-29 19:47:45 -07:00
DESKTOP-GENO133\IvanPlex
d9d0957040 added method to parse markdown data. 2024-01-29 19:43:53 -07:00
Hargata Softworks
9d73db3c51 Merge pull request #184 from hargata/Hargata/reduce.login.error
added env variable to reduce auth-related logs.
2024-01-29 17:21:07 -07:00
DESKTOP-GENO133\IvanPlex
c0f0786fd4 added env variable to reduce auth-related logs. 2024-01-29 17:20:39 -07:00
Hargata Softworks
357eff116f Merge pull request #183 from hargata/Hargata/export.attachments
Export attachments for vehicle.
2024-01-29 13:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
32a047c522 added print function. 2024-01-29 13:32:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8a237bb7ec Export attachments for vehicle. 2024-01-29 13:24:30 -07:00
DESKTOP-GENO133\IvanPlex
f5b9072cc6 Updated Github pages. 2024-01-28 15:06:06 -07:00
Hargata Softworks
4e3eaa53ff Merge pull request #180 from hargata/Hargata/hr-mpg
Hargata/hr mpg
2024-01-28 07:39:42 -07:00
Hargata Softworks
4779d3f161 Merge pull request #181 from hargata/Hargata/move.records
Move records around.
2024-01-28 07:38:04 -07:00
DESKTOP-GENO133\IvanPlex
b0173fae94 added object inheritance. 2024-01-28 07:37:12 -07:00
DESKTOP-GENO133\IvanPlex
c6ee8830a3 added hours per gallon calculation. 2024-01-27 23:29:05 -07:00
DESKTOP-GENO133\IvanPlex
c2eeab5025 Merge commit '43fd40347f34bc7676bfd5531abf612e06285e5c' into Hargata/hr-mpg 2024-01-27 22:53:55 -07:00
DESKTOP-GENO133\IvanPlex
43fd40347f added engine hours variable. 2024-01-27 21:14:29 -07:00
Hargata Softworks
53139f9bb2 Merge pull request #179 from hargata/Hargata/sorting.supplies
added sorting in supplies tab.
2024-01-27 12:17:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0b6033cc00 added sorting in supplies tab. 2024-01-27 12:16:49 -07:00
Hargata Softworks
6f0115e5c5 Merge pull request #176 from hargata/Hargata/pinned.notes.garage
touch screen support for pinned notes.
2024-01-27 07:41:06 -07:00
Hargata Softworks
2403adf537 Merge pull request #177 from hargata/Hargata/sort.columns
make cost columns sortable.
2024-01-27 07:40:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f00ab897b5 make cost columns sortable. 2024-01-27 07:37:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
093631bf6f touch screen support for pinned notes. 2024-01-26 13:19:37 -07:00
Hargata Softworks
5c2835ab76 Merge pull request #175 from hargata/Hargata/pinned.notes.garage
added maxfilesize label and error handling.
2024-01-26 12:42:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
03029981fd added maxfilesize label and error handling. 2024-01-26 12:40:39 -07:00
Hargata Softworks
69b1838038 Merge pull request #174 from hargata/Hargata/pinned.notes.garage
display pinned notes in garage.
2024-01-26 12:02:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1c2f83026c display pinned notes in garage. 2024-01-26 12:01:53 -07:00
Hargata Softworks
d36f2c59e3 Merge pull request #172 from hargata/Hargata/demo.stuff
clear temp files restoring demo instance.
2024-01-26 09:26:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
53ebec3f03 clear temp files restoring demo instance. 2024-01-26 09:25:44 -07:00
Hargata Softworks
f2cbbaeb12 Merge pull request #170 from hargata/Hargata/average.fuel.fix
include partial fuel up fuel consumption in average mpg.
2024-01-25 22:19:02 -07:00
DESKTOP-GENO133\IvanPlex
31c1202649 include partial fuel up fuel consumption in average mpg. 2024-01-25 22:18:33 -07:00
Hargata Softworks
331499461a Merge pull request #148 from hargata/Hargata/documentation
Hargata/documentation
2024-01-25 17:52:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8c6dd5e343 Updated website 2024-01-25 17:52:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
01b1e8228d Merge branch 'main' into Hargata/documentation 2024-01-25 17:01:31 -07:00
Hargata Softworks
43979d6115 Merge pull request #167 from hargata/Hargata/demo.mode
Demo Mode
2024-01-25 15:02:20 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6a9860e202 added demo mode. 2024-01-25 15:01:56 -07:00
Hargata Softworks
39338e8028 Merge pull request #166 from hargata/Hargata/decimal.odometer
added more screenshots
2024-01-25 14:23:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d1d5351a01 added more screenshots 2024-01-25 14:22:21 -07:00
Hargata Softworks
c9385c7fdd Merge pull request #165 from hargata/Hargata/decimal.odometer
updated website.
2024-01-25 14:06:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
afaae89af6 updated website. 2024-01-25 14:05:59 -07:00
Hargata Softworks
b9d799cd49 Merge pull request #164 from hargata/Hargata/decimal.odometer
Prevent decimals in odometer fields to error out.
2024-01-25 13:59:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e47c541e08 Prevent decimals in odometer fields to error out. 2024-01-25 13:58:47 -07:00
Hargata Softworks
51ff01d2cd Merge pull request #162 from hargata/Hargata/locale.gas.fix
more gas fixes due to european locale.
2024-01-25 08:33:25 -07:00
DESKTOP-GENO133\IvanPlex
618399cb09 more gas fixes due to european locale. 2024-01-25 08:31:56 -07:00
Hargata Softworks
b837a2e528 Merge pull request #158 from hargata/Hargata/reminder.email.endpoint
stop method from returning prematurely.
2024-01-24 22:27:32 -07:00
DESKTOP-GENO133\IvanPlex
a1e8b8f9cc stop method from returning prematurely. 2024-01-24 22:26:55 -07:00
Hargata Softworks
787c5da72a Merge pull request #157 from hargata/Hargata/reminder.email.endpoint
added vehicle information.
2024-01-24 22:24:40 -07:00
DESKTOP-GENO133\IvanPlex
260703be8e added vehicle information. 2024-01-24 22:23:31 -07:00
Hargata Softworks
053801b046 Merge pull request #156 from hargata/Hargata/reminder.email.endpoint
Hargata/reminder.email.endpoint
2024-01-24 21:48:15 -07:00
DESKTOP-GENO133\IvanPlex
db9b1970c5 updated API documentation. 2024-01-24 21:47:05 -07:00
DESKTOP-GENO133\IvanPlex
b153ef5ea5 added more mileage intervals and api endpoint to send out reminder emails. 2024-01-24 21:43:26 -07:00
Hargata Softworks
b54809f399 Merge pull request #155 from hargata/Hargata/zero.chart
Baseline zero charges for bar charts.
2024-01-24 09:07:33 -07:00
DESKTOP-GENO133\IvanPlex
f7f47c54ff added baseline zero costs charge so that bar charts display all months regardless of whether there are costs or not. 2024-01-24 09:03:55 -07:00
Hargata Softworks
92564ae527 Merge pull request #151 from hargata/Hargata/recurring.tax
Hargata/recurring.tax
2024-01-23 22:41:51 -07:00
DESKTOP-GENO133\IvanPlex
52ada8574d add pinned icon for pinned notes. 2024-01-23 22:15:53 -07:00
DESKTOP-GENO133\IvanPlex
013fb67943 Recurring fees. 2024-01-23 21:48:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d86298f502 make tax recurring. 2024-01-23 21:10:59 -07:00
Hargata Softworks
5891b78be0 Merge pull request #149 from hargata/Hargata/pin.notes
added ability to pin notes so that they always show up on top.
2024-01-23 17:48:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a9e3e44f2c added ability to pin notes so that they always show up on top. 2024-01-23 17:46:54 -07:00
Hargata Softworks
0d1c7234e8 Create Collaboration.md 2024-01-23 17:22:07 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e3abe5f209 Merge branch 'main' into Hargata/documentation 2024-01-23 17:15:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b453bfce5b removed auth html. 2024-01-23 17:14:53 -07:00
Hargata Softworks
147a1b03a7 Create Registration.md 2024-01-23 16:57:02 -07:00
Hargata Softworks
3017db5f86 Update Configuring.md 2024-01-23 16:45:34 -07:00
Hargata Softworks
399d0d8058 Update GettingStarted.md 2024-01-23 16:43:52 -07:00
Hargata Softworks
b8c0d4ef67 Update GettingStarted.md 2024-01-23 16:43:03 -07:00
Hargata Softworks
918d086705 Update Configuring.md 2024-01-23 16:39:40 -07:00
Hargata Softworks
ac05acf96b Update Configuring.md 2024-01-23 16:37:49 -07:00
Hargata Softworks
e7449806c0 Create Configuring.md 2024-01-23 16:36:46 -07:00
Hargata Softworks
baab3213b5 Update GettingStarted.md 2024-01-23 16:19:41 -07:00
Hargata Softworks
d68e9e9589 Create GettingStarted.md 2024-01-23 16:16:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
dbb139dfad updated documentation. 2024-01-17 12:00:05 -07:00
76 changed files with 1418 additions and 206 deletions

3
.env
View File

@@ -5,4 +5,5 @@ MailConfig__EmailFrom=""
MailConfig__UseSSL="false"
MailConfig__Port=587
MailConfig__Username=""
MailConfig__Password=""
MailConfig__Password=""
LOGGING__LOGLEVEL__DEFAULT=Error

View File

@@ -22,10 +22,13 @@ namespace CarCareTracker.Controllers
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic;
private readonly IFileHelper _fileHelper;
private readonly IMailHelper _mailHelper;
public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper,
IReminderHelper reminderHelper,
@@ -37,6 +40,9 @@ namespace CarCareTracker.Controllers
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper,
IFileHelper fileHelper,
IUserLogic userLogic)
{
@@ -49,6 +55,9 @@ namespace CarCareTracker.Controllers
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper;
_gasHelper = gasHelper;
_reminderHelper = reminderHelper;
_userLogic = userLogic;
@@ -428,12 +437,65 @@ namespace CarCareTracker.Controllers
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/vehicle/reminders/send")]
public IActionResult SendReminders(List<ReminderUrgency> urgencies)
{
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
foreach(Vehicle vehicle in vehicles)
{
var vehicleId = vehicle.Id;
//get reminders
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).OrderByDescending(x => x.Urgency).ToList();
results.RemoveAll(x => !urgencies.Contains(x.Urgency));
if (!results.Any())
{
continue;
}
//get list of recipients.
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
List<string> emailRecipients = new List<string>();
foreach (int userId in userIds)
{
var userData = _userRecordDataAccess.GetUserRecordById(userId);
emailRecipients.Add(userData.EmailAddress);
};
if (!emailRecipients.Any())
{
continue;
}
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
operationResponses.Add(result);
}
if (operationResponses.All(x => x.Success))
{
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
} else if (operationResponses.All(x => !x.Success))
{
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
} else
{
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/makebackup")]
public IActionResult MakeBackup()
{
var result = _fileHelper.MakeBackup();
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/demo/restore")]
public IActionResult RestoreDemo()
{
var result = _fileHelper.RestoreBackup("/defaults/demo_default.zip", true);
return Json(result);
}
private int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();

View File

@@ -85,6 +85,7 @@ namespace CarCareTracker.Controllers
public IActionResult Index(int vehicleId)
{
var data = _dataAccess.GetVehicleById(vehicleId);
UpdateRecurringTaxes(vehicleId);
return View(data);
}
[HttpGet]
@@ -316,7 +317,6 @@ namespace CarCareTracker.Controllers
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
vehicleRecords = vehicleRecords.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
var exportData = convertedRecords.Select(x => new GasRecordExportModel
{
@@ -376,7 +376,7 @@ namespace CarCareTracker.Controllers
{
VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
};
@@ -420,7 +420,7 @@ namespace CarCareTracker.Controllers
{
VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
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)
@@ -433,7 +433,7 @@ namespace CarCareTracker.Controllers
{
VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)),
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
@@ -463,7 +463,7 @@ namespace CarCareTracker.Controllers
{
VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
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)
@@ -476,7 +476,7 @@ namespace CarCareTracker.Controllers
{
VehicleId = vehicleId,
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
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)
@@ -529,8 +529,6 @@ namespace CarCareTracker.Controllers
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
{
var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
//need it in ascending order to perform computation.
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
//check if the user uses MPG or Liters per 100km.
var userConfig = _config.GetUserConfig(User);
bool useMPG = userConfig.UseMPG;
@@ -540,10 +538,13 @@ namespace CarCareTracker.Controllers
{
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
}
var vehicleIsElectric = _dataAccess.GetVehicleById(vehicleId).IsElectric;
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);
@@ -566,9 +567,12 @@ namespace CarCareTracker.Controllers
return Json(result);
}
[HttpGet]
public IActionResult GetAddGasRecordPartialView()
public IActionResult GetAddGasRecordPartialView(int vehicleId)
{
return PartialView("_GasModal", new GasRecordInputContainer() { GasRecord = new GasRecordInput() });
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() });
}
[HttpGet]
public IActionResult GetGasRecordForEditById(int gasRecordId)
@@ -587,10 +591,13 @@ namespace CarCareTracker.Controllers
MissedFuelUp = result.MissedFuelUp,
Notes = result.Notes
};
var vehicleIsElectric = _dataAccess.GetVehicleById(convertedResult.VehicleId).IsElectric;
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);
@@ -757,6 +764,34 @@ namespace CarCareTracker.Controllers
}
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 = recurringFee.Date.AddMonths((int)recurringFee.RecurringInterval);
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,
Files = recurringFee.Files
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
}
}
}
}
[HttpPost]
public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord)
{
@@ -783,6 +818,8 @@ namespace CarCareTracker.Controllers
Description = result.Description,
Notes = result.Notes,
VehicleId = result.VehicleId,
IsRecurring = result.IsRecurring,
RecurringInterval = result.RecurringInterval,
Files = result.Files
};
return PartialView("_TaxRecordModal", convertedResult);
@@ -816,7 +853,7 @@ namespace CarCareTracker.Controllers
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
};
//get costbymonth
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>();
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
@@ -867,10 +904,17 @@ namespace CarCareTracker.Controllers
var userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
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();
viewModel.FuelMileageForVehicleByMonth = monthlyMileageData;
return PartialView("_Report", viewModel);
@@ -937,6 +981,84 @@ namespace CarCareTracker.Controllers
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
{
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
{
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
{
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
{
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
{
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
{
Date = x.Date,
Odometer = x.Mileage,
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();
@@ -956,7 +1078,7 @@ namespace CarCareTracker.Controllers
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
if (gasViewModels.Any())
{
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels);
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG);
}
vehicleHistory.MPG = averageMPG;
//insert servicerecords
@@ -1012,10 +1134,17 @@ namespace CarCareTracker.Controllers
mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year);
}
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
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();
return PartialView("_MPGByMonthReport", monthlyMileageData);
}
@@ -1023,7 +1152,7 @@ namespace CarCareTracker.Controllers
[HttpPost]
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
{
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>();
List<CostForVehicleByMonth> allCosts = StaticHelper.GetBaseLineCosts();
if (selectedMetrics.Contains(ImportMode.ServiceRecord))
{
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
@@ -1266,8 +1395,17 @@ namespace CarCareTracker.Controllers
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)
{
@@ -1558,5 +1696,55 @@ namespace CarCareTracker.Controllers
return Json(result);
}
#endregion
#region "Shared Methods"
public IActionResult MoveRecord(int recordId, ImportMode source, ImportMode destination)
{
var genericRecord = new GenericRecord();
bool result = false;
//get
switch (source)
{
case ImportMode.ServiceRecord:
genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
genericRecord = _collisionRecordDataAccess.GetCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId);
break;
}
//save
switch (destination)
{
case ImportMode.ServiceRecord:
result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord));
break;
case ImportMode.RepairRecord:
result = _collisionRecordDataAccess.SaveCollisionRecordToVehicle(StaticHelper.GenericToRepairRecord(genericRecord));
break;
case ImportMode.UpgradeRecord:
result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord));
break;
}
//delete
if (result)
{
switch (source)
{
case ImportMode.ServiceRecord:
_serviceRecordDataAccess.DeleteServiceRecordById(recordId);
break;
case ImportMode.RepairRecord:
_collisionRecordDataAccess.DeleteCollisionRecordById(recordId);
break;
case ImportMode.UpgradeRecord:
_upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId);
break;
}
}
return Json(result);
}
#endregion
}
}

View File

@@ -14,7 +14,9 @@
FifteenThousandMiles = 15000,
TwentyThousandMiles = 20000,
ThirtyThousandMiles = 30000,
FortyThousandMiles = 40000,
FiftyThousandMiles = 50000,
SixtyThousandMiles = 60000,
OneHundredThousandMiles = 100000,
OneHundredFiftyThousandMiles = 150000
}

View File

@@ -2,6 +2,7 @@
{
public enum ReminderMonthInterval
{
OneMonth = 1,
ThreeMonths = 3,
SixMonths = 6,
OneYear = 12,

View File

@@ -94,6 +94,7 @@ namespace CarCareTracker.Helper
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),

View File

@@ -1,4 +1,5 @@
using System.IO.Compression;
using CarCareTracker.Models;
using System.IO.Compression;
namespace CarCareTracker.Helper
{
@@ -8,7 +9,8 @@ namespace CarCareTracker.Helper
string MoveFileFromTemp(string currentFilePath, string newFolder);
bool DeleteFile(string currentFilePath);
string MakeBackup();
bool RestoreBackup(string fileName);
bool RestoreBackup(string fileName, bool clearExisting = false);
string MakeAttachmentsExport(List<GenericReportModel> exportData);
}
public class FileHelper : IFileHelper
{
@@ -38,7 +40,7 @@ namespace CarCareTracker.Helper
return string.Empty;
}
}
public bool RestoreBackup(string fileName)
public bool RestoreBackup(string fileName, bool clearExisting = false)
{
var fullFilePath = GetFullFilePath(fileName);
if (string.IsNullOrWhiteSpace(fullFilePath))
@@ -64,9 +66,17 @@ namespace CarCareTracker.Helper
{
Directory.CreateDirectory(existingPath);
}
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(imagePath);
foreach(string file in filesToUpload)
foreach (string file in filesToUpload)
{
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
}
@@ -78,6 +88,14 @@ namespace CarCareTracker.Helper
{
Directory.CreateDirectory(existingPath);
}
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(documentPath);
foreach (string file in filesToUpload)
@@ -100,12 +118,37 @@ namespace CarCareTracker.Helper
File.Move(configPath, StaticHelper.UserConfigPath, true);
}
return true;
} catch (Exception ex)
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
return false;
}
}
public string MakeAttachmentsExport(List<GenericReportModel> exportData)
{
var folderName = Guid.NewGuid();
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
int fileIndex = 0;
foreach(GenericReportModel reportModel in exportData)
{
foreach(UploadedFiles file in reportModel.Files)
{
var fileToCopy = GetFullFilePath(file.Location);
var destFileName = $"{tempPath}/{fileIndex}{Path.GetExtension(file.Location)}";
File.Copy(fileToCopy, destFileName);
fileIndex++;
}
}
var destFilePath = $"{tempPath}.zip";
ZipFile.CreateFromDirectory(tempPath, destFilePath);
//delete temp directory
Directory.Delete(tempPath, true);
var zipFileName = $"/temp/{folderName}.zip";
return zipFileName;
}
public string MakeBackup()
{
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";

View File

@@ -5,26 +5,33 @@ namespace CarCareTracker.Helper
public interface IGasHelper
{
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
string GetAverageGasMileage(List<GasRecordViewModel> results);
string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG);
}
public class GasHelper : IGasHelper
{
public string GetAverageGasMileage(List<GasRecordViewModel> results)
public string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG)
{
var recordWithCalculatedMPG = results.Where(x => x.MilesPerGallon > 0);
var recordsToCalculateGallons = results.Where(x => x.MilesPerGallon > 0 || !x.IsFillToFull);
var recordWithCalculatedMPG = recordsToCalculateGallons.Where(x => x.MilesPerGallon > 0);
var minMileage = results.Min(x => x.Mileage);
if (recordWithCalculatedMPG.Any())
{
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
var totalGallonsConsumed = recordWithCalculatedMPG.Sum(x => x.Gallons);
var totalGallonsConsumed = recordsToCalculateGallons.Sum(x => x.Gallons);
var deltaMileage = maxMileage - minMileage;
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed;
if (!useMPG)
{
averageGasMileage = 100 / averageGasMileage;
}
return averageGasMileage.ToString("F");
}
return "0";
}
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
{
//need to order by to get correct results
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
var computedResults = new List<GasRecordViewModel>();
int previousMileage = 0;
decimal unFactoredConsumption = 0.00M;

View File

@@ -8,6 +8,7 @@ namespace CarCareTracker.Helper
{
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
}
public class MailHelper : IMailHelper
{
@@ -60,20 +61,62 @@ namespace CarCareTracker.Helper
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody) {
public OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (!emailAddresses.Any())
{
return new OperationResponse { Success = false, Message = "No recipients could be found" };
}
if (!reminders.Any())
{
return new OperationResponse { Success = false, Message = "No reminders could be found" };
}
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
//construct html table.
string emailBody = $"<h4>{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}</h4><br /><table style='width:100%'><tr><th style='padding:8px;'>Urgency</th><th style='padding:8px;'>Description</th></tr>";
foreach(ReminderRecordViewModel reminder in reminders)
{
emailBody += $"<tr><td style='padding:8px; text-align:center;'>{reminder.Urgency}</td><td style='padding:8px; text-align:center;'>{reminder.Description}</td></tr>";
}
emailBody += "</table>";
try
{
foreach (string emailAddress in emailAddresses)
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
}
return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) {
string to = emailTo;
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
message.IsBodyHtml = isBodyHtml;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
{
client.Send(message);
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
}
else
{
client.Send(message);
}
return true;
}
catch (Exception ex)

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models;
using System.Globalization;
namespace CarCareTracker.Helper
{
@@ -63,5 +64,81 @@ namespace CarCareTracker.Helper
}
return "";
}
public static List<CostForVehicleByMonth> GetBaseLineCosts()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(1), MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(2), MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(3), MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(4), MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(5), MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(6), MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(7), MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(8), MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(9), MonthId = 9, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(10), MonthId = 10, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(11), MonthId = 11, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(12), MonthId = 12, Cost = 0M}
};
}
public static List<CostForVehicleByMonth> GetBaseLineCostsNoMonthName()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth { MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthId = 9, Cost = 0M},
new CostForVehicleByMonth { MonthId = 10, Cost = 0M},
new CostForVehicleByMonth { MonthId = 11, Cost = 0M},
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
};
}
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
{
return new ServiceRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
{
return new CollisionRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
{
return new UpgradeRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
}
}

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models
{
public class CollisionRecord
public class CollisionRecord: GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -2,7 +2,8 @@
{
public class GasRecordInputContainer
{
public bool UseKwh { get; set; }
public GasRecordInput GasRecord { get; set; }
public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public GasRecordInput GasRecord { get; set; }
}
}

View File

@@ -2,7 +2,8 @@
{
public class GasRecordViewModelContainer
{
public bool UseKwh { get; set; }
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
}
}

14
Models/GenericRecord.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace CarCareTracker.Models
{
public class GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -6,5 +6,6 @@
public int VehicleId { get; set; }
public string Description { get; set; }
public string NoteText { get; set; }
public bool Pinned { get; set; }
}
}

View File

@@ -11,5 +11,6 @@
public string Description { get; set; }
public string Notes { get; set; }
public decimal Cost { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models
{
public class ServiceRecord
public class ServiceRecord: GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -8,6 +8,8 @@
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -8,7 +8,18 @@
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public TaxRecord ToTaxRecord() { return new TaxRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Description = Description, Notes = Notes, Files = Files }; }
public TaxRecord ToTaxRecord() { return new TaxRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Description = Description,
Notes = Notes,
IsRecurring = IsRecurring,
RecurringInterval = RecurringInterval,
Files = Files }; }
}
}

View File

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models
{
public class UpgradeRecord
public class UpgradeRecord: GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -10,6 +10,7 @@
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
public bool UseMarkDownOnSavedNotes { get; set; }
public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; }
public string UserNameHash { get; set; }

View File

@@ -9,5 +9,6 @@
public string Model { get; set; }
public string LicensePlate { get; set; }
public bool IsElectric { get; set; } = false;
public bool UseHours { get; set; } = false;
}
}

View File

@@ -2,6 +2,8 @@
A self-hosted, open-source vehicle service records and maintainence tracker.
Visit our website: https://lubelogger.com
Support this project on Patreon: https://patreon.com/LubeLogger
## Why
@@ -10,6 +12,11 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
## Screenshots
<a href="/docs/screenshots.md">Screenshots</a>
## Demo
Try it out before you download it! The live demo resets every 20 minutes.
[Live Demo](https://demo.lubelogger.com) Login using username "test" and password "1234"
## Dependencies
- Bootstrap
- LiteDB
@@ -17,12 +24,13 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- SweetAlert2
- CsvHelper
- Chart.js
- Drawdown
## Docker Setup (GHCR)
1. Install Docker
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats. You can also setup SMTP Config here.
4. If not using traefik, use docker-compose-notraefik.yml
4. If using traefik, use docker-compose.traefik.yml
5. Run `docker-compose up`
## Docker Setup (Manual Build)
@@ -31,7 +39,7 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If not using traefik, use docker-compose-notraefik.yml
6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up`
## Additional Docker Instructions

View File

@@ -241,6 +241,21 @@
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/reminders/send</code>
</div>
<div class="col-3">
Send reminder emails out to collaborators based on specified urgency.
</div>
<div class="col-3">
(must be root user)<br />
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
</div>
</div>
<div class="row">
<div class="col-1">
GET

View File

@@ -4,7 +4,7 @@
{
foreach (Vehicle vehicle in Model)
{
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4">
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none" id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
<div class="card" onclick="viewVehicle(@vehicle.Id)">
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
<div class="card-body">

View File

@@ -35,6 +35,10 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UseThreeDecimalGasCost">
<label class="form-check-label" for="useThreeDecimal">Use Three Decimals For Fuel Cost</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMarkDownOnSavedNotes" checked="@Model.UseMarkDownOnSavedNotes">
<label class="form-check-label" for="useMarkDownOnSavedNotes">Display Saved Notes in Markdown</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoReminderRefresh" checked="@Model.EnableAutoReminderRefresh">
<label class="form-check-label" for="enableAutoReminderRefresh">Auto Refresh Lapsed Recurring Reminders</label>
@@ -154,7 +158,7 @@
<img src="/defaults/lubelogger_logo.png" />
</div>
<div class="d-flex justify-content-center">
<small class="text-body-secondary">Version 1.0.7</small>
<small class="text-body-secondary">Version 1.1.0</small>
</div>
<p class="lead">
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
@@ -187,6 +191,7 @@
<li class="list-group-item">SweetAlert2</li>
<li class="list-group-item">CsvHelper</li>
<li class="list-group-item">Chart.js</li>
<li class="list-group-item">Drawdown</li>
</ul>
</div>
</div>
@@ -211,6 +216,7 @@
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
visibleTabs: visibleTabs,

View File

@@ -5,6 +5,7 @@
var userConfig = config.GetUserConfig(User);
var useDarkMode = userConfig.UseDarkMode;
var enableCsvImports = userConfig.EnableCsvImports;
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
shortDatePattern = shortDatePattern.ToLower();
@@ -47,7 +48,8 @@
function getGlobalConfig() {
return {
useDarkMode : "@useDarkMode" == "True",
enableCsvImport : "@enableCsvImports" == "True"
enableCsvImport : "@enableCsvImports" == "True",
useMarkDown: "@useMarkDown" == "True"
}
}
function getShortDatePattern() {
@@ -59,10 +61,16 @@
//remove thousands separator.
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
var currencySymbol = "@numberFormat.CurrencySymbol";
if (input == "---") {
input = "0";
}
//strip thousands from input.
input = input.replace(thousandSeparator, "");
//convert to JS format where decimal is only separated by .
input = input.replace(decimalSeparator, ".");
//remove any currency symbol
input = input.replace(currencySymbol, "");
return parseFloat(input);
}
function globalFloatToString(input) {

View File

@@ -21,6 +21,7 @@
<script src="~/js/planrecord.js" asp-append-version="true"></script>
<script src="~/js/odometerrecord.js" asp-append-version="true"></script>
<script src="~/lib/chart-js/chart.umd.js"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">

View File

@@ -29,7 +29,7 @@
}
</div>
<div class="col-md-6 col-12">
<label for="collisionRecordNotes">Notes(optional)</label>
<label for="collisionRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -37,6 +37,7 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="collisionRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
@@ -52,6 +53,7 @@
}
<label for="collisionRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>
@@ -61,7 +63,17 @@
<div class="modal-footer">
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteCollisionRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteCollisionRecord(@Model.Id)">Delete</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Move To</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'RepairRecord', 'ServiceRecord')">Service Records</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'RepairRecord', 'UpgradeRecord')">Upgrades</a></li>
</ul>
</div>
}
<button type="button" class="btn btn-secondary" onclick="hideAddCollisionRecordModal()">Cancel</button>
@if (isNew)

View File

@@ -22,6 +22,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}
@@ -45,7 +47,7 @@
<th scope="col" class="col-2 col-xl-1">Date</th>
<th scope="col" class="col-2">Odometer</th>
<th scope="col" class="col-3 col-xl-4">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-2" onclick="toggleSort('accident-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
</thead>

View File

@@ -12,14 +12,14 @@
labels: ["Service Records", "Repairs", "Upgrades", "Tax", "Fuel"],
datasets: [
{
label: "Expenses by Category",
label: "Expenses by Type",
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
data: [
@Model.ServiceRecordSum,
@Model.CollisionRecordSum,
@Model.UpgradeRecordSum,
@Model.TaxRecordSum,
@Model.GasRecordSum
globalParseFloat('@Model.ServiceRecordSum'),
globalParseFloat('@Model.CollisionRecordSum'),
globalParseFloat('@Model.UpgradeRecordSum'),
globalParseFloat('@Model.TaxRecordSum'),
globalParseFloat('@Model.GasRecordSum')
]
}
]

View File

@@ -11,28 +11,29 @@
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
var gasCostFormat = useThreeDecimals ? "C3" : "C2";
var useKwh = Model.UseKwh;
var useHours = Model.UseHours;
string consumptionUnit;
string fuelEconomyUnit;
string distanceUnit = useMPG ? "mi." : "km";
string distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
if (useKwh)
{
consumptionUnit = "kWh";
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
}
else if (useMPG && useUKMPG)
{
consumptionUnit = "imp gal";
fuelEconomyUnit = "mpg";
fuelEconomyUnit = useHours ? "h/g" : "mpg";
} else if (useUKMPG)
{
fuelEconomyUnit = "l/100mi.";
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
consumptionUnit = "l";
distanceUnit = "mi.";
distanceUnit = useHours ? "h" : "mi.";
}
else
{
consumptionUnit = useMPG ? "US gal" : "l";
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
}
}
<div class="row">
@@ -41,7 +42,7 @@
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
{
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {gasHelper.GetAverageGasMileage(Model.GasRecords)}")</span>
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}")</span>
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
}
@@ -58,6 +59,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('GasRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('GasRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
} else {
@@ -77,10 +80,10 @@
<tr class="d-flex">
<th scope="col" class="col-2">Date Refueled</th>
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
<th scope="col" class="col-2">Consumption(@(consumptionUnit))</th>
<th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</th>
<th scope="col" class="col-1">Cost</th>
<th scope="col" class="col-1">Unit Cost</th>
<th scope="col" class="col-2" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Consumption(@(consumptionUnit))</th>
<th scope="col" class="col-4" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Fuel Economy(@(fuelEconomyUnit))</th>
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Unit Cost</th>
</tr>
</thead>
<tbody>

View File

@@ -3,7 +3,7 @@
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
var sortedByMPG = Model.OrderBy(x => x.Cost).ToList();
}
@if (Model.Any())
@if (Model.Where(x=>x.Cost > 0).Any())
{
<canvas id="bar-chart"></canvas>
<script>
@@ -17,7 +17,7 @@
@foreach (CostForVehicleByMonth gasCost in Model)
{
@:barGraphLabels.push("@gasCost.MonthName");
@:barGraphData.push(@gasCost.Cost);
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
@:barGraphColors.push('@barGraphColors[index]');
}

View File

@@ -5,6 +5,7 @@
var useMPG = config.GetUserConfig(User).UseMPG;
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
var useKwh = Model.UseKwh;
var useHours = Model.UseHours;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;
string distanceUnit;
@@ -19,7 +20,11 @@
{
consumptionUnit = useMPG ? "gallons" : "liters";
}
if (useUKMPG)
if (useHours)
{
distanceUnit = "hours";
}
else if (useUKMPG)
{
distanceUnit = "miles";
}
@@ -73,7 +78,7 @@
}
</div>
<div class="col-md-6 col-12">
<label for="gasRecordNotes">Notes(optional)</label>
<label for="gasRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="gasRecordNotes" class="form-control" rows="5">@Model.GasRecord.Notes</textarea>
@if (Model.GasRecord.Files.Any())
{
@@ -81,12 +86,14 @@
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
<label for="gasRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
{
<label for="gasRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>

View File

@@ -3,7 +3,7 @@
var barGraphColors = new string[] { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" };
var sortedByMPG = Model.OrderByDescending(x => x.Cost).ToList();
}
@if (Model.Any())
@if (Model.Where(x=>x.Cost > 0).Any())
{
<canvas id="bar-chart-mpg"></canvas>
@@ -18,7 +18,7 @@
@foreach (CostForVehicleByMonth gasCost in Model)
{
@:barGraphLabels.push("@gasCost.MonthName");
@:barGraphData.push(@gasCost.Cost);
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
@:barGraphColors.push('@barGraphColors[index]');
}

View File

@@ -12,11 +12,15 @@
<div class="row">
<div class="col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="noteIsPinned" checked="@Model.Pinned">
<label class="form-check-label" for="noteIsPinned">Pinned</label>
</div>
<label for="noteDescription">Description</label>
<input type="text" id="noteDescription" class="form-control" placeholder="Description of the note" value="@(isNew ? "" : Model.Description)">
</div>
<div class="col-12">
<label for="noteTextArea">Notes</label>
<label for="noteTextArea">Notes<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea>
</div>
</div>

View File

@@ -27,7 +27,13 @@
@foreach (Note note in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)">
<td class="col-3">@note.Description</td>
@if (note.Pinned)
{
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description</td>
} else
{
<td class="col-3">@note.Description</td>
}
<td class="col-9 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(note.NoteText, 100)</td>
</tr>
}

View File

@@ -21,7 +21,7 @@
<input type="number" id="odometerRecordMileage" class="form-control" placeholder="Odometer reading" value="@(isNew ? "" : Model.Mileage)">
</div>
<div class="col-md-6 col-12">
<label for="odometerRecordNotes">Notes(optional)</label>
<label for="odometerRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="odometerRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -29,12 +29,14 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="odometerRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="odometerRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
{
<label for="odometerRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="odometerRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>

View File

@@ -21,6 +21,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('OdometerRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}

View File

@@ -45,7 +45,7 @@
}
</div>
<div class="col-md-6 col-12">
<label for="planRecordNotes">Notes(optional)</label>
<label for="planRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="planRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -53,12 +53,14 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="planRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
{
<label for="planRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="planRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>

View File

@@ -39,7 +39,7 @@
</div>
</div>
<div class="col-md-6 col-12">
<label for="reminderNotes">Notes(optional)</label>
<label for="reminderNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" onChange="enableRecurring()" role="switch" id="reminderIsRecurring" checked="@Model.IsRecurring">
@@ -59,7 +59,9 @@
<!option value="FifteenThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FifteenThousandMiles ? "selected" : "")>15000 mi. / Km</!option>
<!option value="TwentyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.TwentyThousandMiles ? "selected" : "")>20000 mi. / Km</!option>
<!option value="ThirtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.ThirtyThousandMiles ? "selected" : "")>30000 mi. / Km</!option>
<!option value="FortyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FortyThousandMiles ? "selected" : "")>40000 mi. / Km</!option>
<!option value="FiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyThousandMiles ? "selected" : "")>50000 mi. / Km</!option>
<!option value="SixtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.SixtyThousandMiles ? "selected" : "")>60000 mi. / Km</!option>
<!option value="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option>
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
</select>

View File

@@ -1,90 +1,93 @@
@model ReportViewModel
<div class="container reportTabContainer">
<div class="row hideOnPrint">
<div class="col-md-3 col-12 mt-2">
<div class="row">
<div class="col-12">
<select class="form-select" id="yearOption" onchange="yearUpdated()">
<option value="0">All Time</option>
@foreach (int year in Model.Years)
{
<option value="@year">@year</option>
}
</select>
<div class="row hideOnPrint">
<div class="col-md-3 col-12 mt-2">
<div class="row">
<div class="col-12">
<select class="form-select" id="yearOption" onchange="yearUpdated()">
<option value="0">All Time</option>
@foreach (int year in Model.Years)
{
<option value="@year">@year</option>
}
</select>
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="costMakeUpReportContent">
@await Html.PartialAsync("_CostMakeUpReport", Model.CostMakeUpForVehicle)
</div>
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="costMakeUpReportContent">
@await Html.PartialAsync("_CostMakeUpReport", Model.CostMakeUpForVehicle)
</div>
</div>
</div>
<div class="col-md-6 col-12 mt-2">
<div class="row">
<div class="col-md-1 d-sm-none d-md-block"></div>
<div class="col-12 col-md-10 reportsCheckBoxContainer">
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
<label class="form-check-label" for="serviceExpenseCheck">Service</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
<label class="form-check-label" for="repairExpenseCheck">Repairs</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
<label class="form-check-label" for="upgradeExpenseCheck">Upgrades</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
<label class="form-check-label" for="gasExpenseCheck">Gas</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
<label class="form-check-label" for="taxExpenseCheck">Tax</label>
</div>
</div>
<div class="col-md-6 col-12 mt-2">
<div class="row">
<div class="col-md-1 d-sm-none d-md-block"></div>
<div class="col-12 col-md-10 reportsCheckBoxContainer">
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="serviceExpenseCheck" value="1" checked>
<label class="form-check-label" for="serviceExpenseCheck">Service</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="repairExpenseCheck" value="2" checked>
<label class="form-check-label" for="repairExpenseCheck">Repairs</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
<label class="form-check-label" for="upgradeExpenseCheck">Upgrades</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="gasExpenseCheck" value="4" checked>
<label class="form-check-label" for="gasExpenseCheck">Fuel</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" onChange="updateCheck()" type="checkbox" id="taxExpenseCheck" value="5" checked>
<label class="form-check-label" for="taxExpenseCheck">Taxes</label>
</div>
</div>
<div class="col-md-1 d-sm-none d-md-block"></div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="gasCostByMonthReportContent">
@await Html.PartialAsync("_GasCostByMonthReport", Model.CostForVehicleByMonth)
</div>
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="gasCostByMonthReportContent">
@await Html.PartialAsync("_GasCostByMonthReport", Model.CostForVehicleByMonth)
<div class="col-md-3 col-12 mt-2">
<div class="row">
<div class="col-12">
<select class="form-select" onchange="updateReminderPie()" id="reminderOption">
<option value="0">As of Today</option>
<option value="30">+30 Days</option>
<option value="60">+60 Days</option>
<option value="90">+90 Days</option>
</select>
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="reminderMakeUpReportContent">
@await Html.PartialAsync("_ReminderMakeUpReport", Model.ReminderMakeUpForVehicle)
</div>
</div>
</div>
</div>
<div class="col-md-3 col-12 mt-2">
<div class="row">
<div class="col-12">
<select class="form-select" onchange="updateReminderPie()" id="reminderOption">
<option value="0">As of Today</option>
<option value="30">+30 Days</option>
<option value="60">+60 Days</option>
<option value="90">+90 Days</option>
</select>
<hr />
<div class="row hideOnPrint">
<div class="col-md-3 col-12 chartContainer" id="collaboratorContent">
@await Html.PartialAsync("_Collaborators", Model.Collaborators)
</div>
<div class="col-md-6 col-12 chartContainer">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="monthFuelMileageReportContent">
@await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth)
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="reminderMakeUpReportContent">
@await Html.PartialAsync("_ReminderMakeUpReport", Model.ReminderMakeUpForVehicle)
<div class="col-md-3 col-12 chartContainer">
<div class="d-grid">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">Vehicle Maintenance Report<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
<div class="d-grid">
<button onclick="exportAttachments()" class="btn btn-secondary btn-md mt-1 mb-1">Export Attachments<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
</div>
</div>
</div>
<hr />
<div class="row hideOnPrint">
<div class="col-md-3 col-12 chartContainer" id="collaboratorContent">
@await Html.PartialAsync("_Collaborators", Model.Collaborators)
</div>
<div class="col-md-6 col-12 chartContainer">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="monthFuelMileageReportContent">
@await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth)
</div>
</div>
<div class="col-md-3 col-12 chartContainer">
<div class="d-flex justify-content-center">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">Vehicle Maintenance Report<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
</div>
</div>
</div>
<div id="vehicleHistoryReport" class="showOnPrint"></div>

View File

@@ -29,7 +29,7 @@
}
</div>
<div class="col-md-6 col-12">
<label for="serviceRecordNotes">Notes(optional)</label>
<label for="serviceRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -37,6 +37,7 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="serviceRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
@@ -52,6 +53,7 @@
}
<label for="serviceRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>
@@ -61,7 +63,17 @@
<div class="modal-footer">
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteServiceRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteServiceRecord(@Model.Id)" >Delete</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Move To</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'ServiceRecord', 'RepairRecord')">Repairs</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'ServiceRecord', 'UpgradeRecord')">Upgrades</a></li>
</ul>
</div>
}
<button type="button" class="btn btn-secondary" onclick="hideAddServiceRecordModal()">Cancel</button>
@if (isNew)

View File

@@ -22,6 +22,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('ServiceRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('ServiceRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}
@@ -45,7 +47,7 @@
<th scope="col" class="col-2 col-xl-1">Date</th>
<th scope="col" class="col-2">Odometer</th>
<th scope="col" class="col-3 col-xl-4">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-2" onclick="toggleSort('servicerecord-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
</thead>

View File

@@ -35,7 +35,7 @@
</div>
</div>
<div class="col-md-6 col-12">
<label for="supplyRecordNotes">Notes(optional)</label>
<label for="supplyRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="supplyRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -43,12 +43,14 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="supplyRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
{
<label for="supplyRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="supplyRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>

View File

@@ -22,6 +22,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('SupplyRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('SupplyRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}
@@ -46,8 +48,8 @@
<th scope="col" class="col-2">Part #</th>
<th scope="col" class="col-2">Supplier</th>
<th scope="col" class="col-2 col-xl-3">Description</th>
<th scope="col" class="col-1">Quantity</th>
<th scope="col" class="col-1">Cost</th>
<th scope="col" class="col-1" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">Quantity</th>
<th scope="col" class="col-1" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-2">Notes</th>
</tr>
</thead>

View File

@@ -23,14 +23,29 @@
<input type="text" id="taxRecordCost" class="form-control" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)">
</div>
<div class="col-md-6 col-12">
<label for="taxRecordNotes">Notes(optional)</label>
<label for="taxRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring">
<label class="form-check-label" for="taxIsRecurring">Is Recurring</label>
</div>
<label for="taxRecurringMonth">Month</label>
<select class="form-select" id="taxRecurringMonth" @(Model.IsRecurring ? "" : "disabled")>
<!option value="OneMonth" @(Model.RecurringInterval == ReminderMonthInterval.OneMonth ? "selected" : "")>1 Month</!option>
<!option value="ThreeMonths" @(Model.RecurringInterval == ReminderMonthInterval.ThreeMonths || isNew ? "selected" : "")>3 Months</!option>
<!option value="SixMonths" @(Model.RecurringInterval == ReminderMonthInterval.SixMonths ? "selected" : "")>6 Months</!option>
<!option value="OneYear" @(Model.RecurringInterval == ReminderMonthInterval.OneYear ? "selected" : "")>1 Year</!option>
<!option value="TwoYears" @(Model.RecurringInterval == ReminderMonthInterval.TwoYears ? "selected" : "")>2 Years</!option>
<!option value="ThreeYears" @(Model.RecurringInterval == ReminderMonthInterval.ThreeYears ? "selected" : "")>3 Years</!option>
<!option value="FiveYears" @(Model.RecurringInterval == ReminderMonthInterval.FiveYears ? "selected" : "")>5 Years</!option>
</select>
@if (Model.Files.Any())
{
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="taxRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
@@ -46,6 +61,7 @@
}
<label for="taxRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>

View File

@@ -22,6 +22,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('TaxRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('TaxRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}
@@ -44,7 +46,7 @@
<tr class="d-flex">
<th scope="col" class="col-3 col-xl-1">Date</th>
<th scope="col" class="col-4 col-xl-6">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-2" onclick="toggleSort('tax-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
</thead>

View File

@@ -29,7 +29,7 @@
}
</div>
<div class="col-md-6 col-12">
<label for="upgradeRecordNotes">Notes(optional)</label>
<label for="upgradeRecordNotes">Notes(optional)<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
@@ -37,6 +37,7 @@
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="upgradeRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
</div>
}
else
@@ -52,6 +53,7 @@
}
<label for="upgradeRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
<br /><small class="text-body-secondary">Max File Size: 28.6MB</small>
}
</div>
</div>
@@ -61,7 +63,17 @@
<div class="modal-footer">
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteUpgradeRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
<div class="btn-group" style="margin-right:auto;">
<button type="button" class="btn btn-md mt-1 mb-1 btn-danger" onclick="deleteUpgradeRecord(@Model.Id)">Delete</button>
<button type="button" class="btn btn-md btn-danger btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Move To</h6></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'UpgradeRecord', 'ServiceRecord')">Service Records</a></li>
<li><a class="dropdown-item" href="#" onclick="moveRecord(@Model.Id, 'UpgradeRecord', 'RepairRecord')">Repairs</a></li>
</ul>
</div>
}
<button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">Cancel</button>
@if (isNew)

View File

@@ -22,6 +22,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('UpgradeRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('UpgradeRecord')">Export to CSV</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="printTab()">Print</a></li>
</ul>
</div>
}
@@ -45,7 +47,7 @@
<th scope="col" class="col-2 col-xl-1">Date</th>
<th scope="col" class="col-2">Odometer</th>
<th scope="col" class="col-3 col-xl-4">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-2" onclick="toggleSort('upgrade-tab-pane', this)" style="cursor:pointer;">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
</thead>

View File

@@ -5,22 +5,24 @@
var useMPG = config.GetUserConfig(User).UseMPG;
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
var useKwh = Model.VehicleData.IsElectric;
var useHours = Model.VehicleData.UseHours;
string fuelEconomyUnit;
if (useKwh)
{
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
var distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
}
else if (useMPG && useUKMPG)
{
fuelEconomyUnit = "mpg";
fuelEconomyUnit = useHours ? "h/g" : "mpg";
}
else if (useUKMPG)
{
fuelEconomyUnit = "l/100mi.";
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
}
else
{
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
}
}
@model VehicleHistoryViewModel

View File

@@ -33,6 +33,10 @@
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
<label class="form-check-label" for="inputIsElectric">Electric Vehicle</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputUseHours" checked="@Model.UseHours">
<label class="form-check-label" for="inputUseHours">Use Engine Hours</label>
</div>
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
{
<label for="inputImage">Replace picture(optional)</label>

View File

@@ -16,6 +16,7 @@
"EnableAutoOdometerInsert": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,
"UseMarkDownOnSavedNotes": false,
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
"DefaultTab": 8,
"UserNameHash": "",

BIN
docs/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,21 @@
# Sharing your Vehicles and Collaborating with Other Users
LubeLogger allows you to collaborate on vehicles so that more than one user can add or edit records on a vehicle.
To share a vehicle, simply navigate into the Vehicle's Dashboard, and look to the bottom left
![image](https://github.com/hargata/lubelog/assets/155338622/76eda35f-5547-47d9-91cb-3fbf30d2a3d9)
Click on the little blue button with a User Add icon and you will be prompted to enter the user's username
![image](https://github.com/hargata/lubelog/assets/155338622/6bbcc309-a790-48e0-91fa-24c57df20531)
**Note:** The username is case sensitive and the user must exist in the system.
Once you have added the user, their username will then show up in the list of Collaborators
![image](https://github.com/hargata/lubelog/assets/155338622/edac8c3b-eb3e-411d-ab45-e54cb66206f3)
Now the user can view and edit the vehicle as well:
![image](https://github.com/hargata/lubelog/assets/155338622/85b0aacf-ae5c-4361-8d1c-32f9ec637a5b)

View File

@@ -0,0 +1,51 @@
# Configuring LubeLogger
In order to provide the best possible user experience, we have provided ample amount of flexibility when it comes to user settings.
Upon initial launch, you are using the Root User by default without any authentication, so you will have access to all of the settings.
![image](https://github.com/hargata/lubelog/assets/155338622/cf73ff68-e79b-468d-84fb-375e481cbadd)
Most of the settings are relatively straightforward and self-explanatory.
**Note:** If you are a user in the UK and you wish be able to input Fuel Purchases in Liters but display Fuel Mileage as Miles Per UK Gallons, you will need to enable "Use Imperial Calculation" and "Use UK MPG Calculation"
**Note:** When making changes to Settings as a root user, your settings will be saved and served up as the default server settings for any new users that sign up.
## Enable Authentication
It is highly recommended that you secure your LubeLogger instance by enabling authentication.
To do so, simply check "Enable Authentication" and you will be prompted to enter a Username and Password
![image](https://github.com/hargata/lubelog/assets/155338622/4d8b1855-e437-4ade-999b-3dd6ea19e55f)
The credentials that you set up here are the credentials for the Root User, aka the Super Admin, and shouldn't be shared with anyone else.
Once you have entered the credentials, you will then be redirected to a Login page
![image](https://github.com/hargata/lubelog/assets/155338622/26181116-5a03-48cf-972d-89a4b1050cce)
Simply enter the credentials you have just set up and you will be logged right in
![image](https://github.com/hargata/lubelog/assets/155338622/20d9a3b9-80de-4a32-8cb6-1100dd237dbd)
## Setting Up Multiple Users
To set up multiple users, all you have to do is click on the dropdown that has your username on it and select "Admin Panel"
![image](https://github.com/hargata/lubelog/assets/155338622/75f32408-b9f3-4e2c-bec6-ea1c196c3438)
If you have SMTP configured correctly, the "Auto Notify(via Email) switch will be enabled and checked, otherwise it will be disabled/grayed out.
Without SMTP Configured:
![image](https://github.com/hargata/lubelog/assets/155338622/9d75ab55-afb7-4d3e-b830-3191de23695c)
With SMTP Configured:
![image](https://github.com/hargata/lubelog/assets/155338622/d9995c30-dd47-44d1-a73d-39f0f129450b)
To generate a new user token, simply click on the "Generate User Token" button and you will be prompted with the user's email address
![image](https://github.com/hargata/lubelog/assets/155338622/f870d5cd-4d4b-480c-94ea-b1161757793f)
If you have SMTP Configured, the user will then receive an Email that looks similar to this:
![image](https://github.com/hargata/lubelog/assets/155338622/0d5c1adf-43d1-45a5-ad59-91fb3b1476af)
The user can then proceed to Register for an account at your instance of LubeLogger.

View File

@@ -0,0 +1,75 @@
# Getting Started
## Docker
The Docker Container Repository is the most reliable and up-to-date distribution channel for LubeLogger.
You need to have Docker Windows installed and Virtualization enabled(typically a BIOS setting).
You will then clone the following files onto your computer from the repository _.env_ and _docker-compose.yml_ or _docker-compose-traefik.yml_ if you're using Traefik.
In the .env file you will find the following and here are the explanations for the variables.
```
LC_ALL=en_US.UTF-8 <- Locale and Language Settings, this will affect how numbers, currencies, and dates are formatted.
LANG=en_US.UTF-8 <- Same as above. Note that some languages don't have UTF-8 encodings.
MailConfig__EmailServer="" <- Email SMTP settings used only for configuring multiple users(to send their registration token and forgot password tokens)
MailConfig__EmailFrom="" <- Same as above.
MailConfig__UseSSL="false" <- Same as above.
MailConfig__Port=587 <- Same as above.
MailConfig__Username="" <- Same as above.
MailConfig__Password="" <- Same as above.
```
Once you're happy with the configuration, run the following commands to pull down the image and run container.
```
docker pull ghcr.io/hargata/lubelogger:latest
docker-compose up
```
By default the app will start listening at localhost:8080, this port can be configured in the docker-compose file.
## Windows Standalone Executable
Windows Standalone executables are provided on a request basis, and will usually be included with every other release.
To run the server, you just have to double click on CarCareTracker.exe
Occassionally you might run into an issue regarding a missing folder, to fix that, just create a "config" folder where CarCareTracker.exe is located.
If you wish to set up SMTP when using this approach, you will have to configure the environment settings in appsettings.json located in the same folder as CarCareTracker.exe
You just have to add the MailConfig section into it, but I provided the full appsettings.json anyways as an example.
```
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"UseDarkMode": false,
"EnableCsvImports": true,
"UseMPG": true,
"UseDescending": false,
"EnableAuth": false,
"HideZero": false,
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
"DefaultTab": 8,
"UserNameHash": "",
"UserPasswordHash": "",
"MailConfig": {
"EmailServer": "",
"EmailFrom": "",
"UseSSL": true,
"Port": 587,
"Username": "",
"Password": ""
}
}
```
When using this approach, the default port the app will be listening on is 5000, so you will navigate to localhost:5000
## Test that It Works
Whichever path you choose, once you get the app up and running, just navigate to the IP address and port the server is listening to and you should be able to see the app
![image](https://github.com/hargata/lubelog/assets/155338622/a32c40ce-271c-406b-a211-c1f2af138418)
![image](https://github.com/hargata/lubelog/assets/155338622/4e1c913c-001e-43fd-9b56-4ecffdfd4c2e)

View File

@@ -0,0 +1,22 @@
# Registration
Once they user have receive an email containing their LubeLogger token(pictured below), they can then register for an account.
![image](https://github.com/hargata/lubelog/assets/155338622/80ef7208-bfaa-4cab-ba0e-c0a3ed6f255e)
They can do so by navigating to your LubeLogger instance and clicking on the "Register" link.
![image](https://github.com/hargata/lubelog/assets/155338622/99c1538d-5cfa-4ac8-8946-ef9eb7dfef9a)
Then they just have to provide the token and their email address along with their set of credentials
![image](https://github.com/hargata/lubelog/assets/155338622/c55d7d2d-1366-4fcf-970f-513e92a93c7a)
**Note:** The email address and token are CASE SENSITIVE and MUST be identical to the email address that the token is generated for.
![image](https://github.com/hargata/lubelog/assets/155338622/b77d49ef-d3e3-43bf-9792-360f305ad379)
Once the user has registered successfully, they can then log in using their credentials.
You can also verify that in your panel the token is deleted and the user now shows up under list of users:
![image](https://github.com/hargata/lubelog/assets/155338622/2f4a622a-cb44-4ba9-8b5d-691f677fee3d)

View File

@@ -25,8 +25,16 @@
<h6 class="display-6 text-center">Self-Hosted, Open-Source, Unconventionally-Named Vehicle Maintenance Records and Fuel Mileage Tracker</h6>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#showcase">Showcase</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#features">Features</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#demo">Demo</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="#download">Download</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://docs.lubelogger.com">Docs</a></div>
<div class="col-12 col-sm-6 col-md-2"><a class="btn btn-dark" href="https://github.com/hargata/lubelog">GitHub Repo</a></div>
</div>
<hr>
<div class="row" id="showcase">
<div class="col-12 d-flex justify-content-center">
<h6 class="display-6 text-center">Showcase</h6>
</div>
@@ -38,6 +46,8 @@
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="1"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="2"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="3"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="4"></button>
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="5"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
@@ -47,6 +57,20 @@
<p>All of your vehicles conveniently displayed in one place</p>
</div>
</div>
<div class="carousel-item">
<img src="dashboard.png" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block customCarouselCaption">
<h5>Dashboard</h5>
<p>Get an overview of your vehicle expenses</p>
</div>
</div>
<div class="carousel-item">
<img src="planner.png" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block customCarouselCaption">
<h5>Planner(Kanban Board)</h5>
<p>Plan and track the progress of your To-Do's</p>
</div>
</div>
<div class="carousel-item">
<img src="servicerecord.png" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block customCarouselCaption">
@@ -80,7 +104,7 @@
</div>
</div>
<hr>
<div class="row">
<div class="row" id="features">
<div class="col-12 d-flex justify-content-center">
<h6 class="display-6 text-center">Features</h6>
</div>
@@ -91,24 +115,41 @@
<li class="list-group-item">Keeps track of all your maintenance, repair, and upgrade records</li>
<li class="list-group-item">Keeps track of your fuel economy(supports MPG, UK MPG, and L/100KM)</li>
<li class="list-group-item">Keeps track of taxes(registration, going fast tax, etc)</li>
<li class="list-group-item">Keeps track of supplies(parts, fluids, etc)</li>
<li class="list-group-item">No limit on how many vehicles you have in your garage</li>
<li class="list-group-item">Import existing records from CSV(supports imports from Fuelly for Fuel Records)</li>
<li class="list-group-item">Import existing records from CSV(supports imports from Fuelly)</li>
<li class="list-group-item">Attach documents for each record(receipts, invoices, etc)</li>
</ul>
</div>
<div class="col-12 col-md-6">
<ul class="list-group">
<li class="list-group-item">Set reminders so you never miss another scheduled maintenance</li>
<li class="list-group-item">Keeps track of your To-Do's(Kanban Planner)</li>
<li class="list-group-item">Set recurring reminders so you never miss another scheduled maintenance</li>
<li class="list-group-item">Dark Mode</li>
<li class="list-group-item">Mobile/Small screen support</li>
<li class="list-group-item">Basic Authentication for security</li>
<li class="list-group-item">Coming Soon(API Endpoints)</li>
<li class="list-group-item">Coming Soon(Consolidated Report Export - Just like CarFax)</li>
<li class="list-group-item">API Endpoints</li>
<li class="list-group-item">Consolidated Vehicle Maintenance Report</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="row" id="demo">
<div class="col-12 d-flex justify-content-center">
<h6 class="display-6 text-center">Try It Out</h6>
</div>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Live demo available <a href="https://demo.lubelogger.com">here</a>
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">Login to the demo using the username "test" and password "1234". The demo site resets every 20 minutes.
</p>
</div>
<hr>
<div class="row" id="download">
<div class="col-12 d-flex justify-content-center">
<h6 class="display-6 text-center">Where to Download</h6>
</div>
@@ -152,7 +193,14 @@
<hr>
<div class="row">
<div class="col-12 d-flex justify-content-center">
<p class="lead">LubeLogger is proudly developed in Price Utah by Hargata Softworks
<h6 class="display-6 text-center">About</h6>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">LubeLogger is proudly developed in Price Utah by Hargata Softworks.
</p>
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead"><a href="https://www.patreon.com/LubeLogger">Support us on Patreon</a>
</p>
</div>
<div class="col-12 d-flex justify-content-center">

BIN
docs/planner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

View File

@@ -15,6 +15,11 @@ function showEditCollisionRecordModal(collisionRecordId) {
//initiate datepicker
initDatePicker($('#collisionRecordDate'));
$('#collisionRecordModal').modal('show');
$('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("collisionRecordNotes");
}
});
}
});
}
@@ -71,7 +76,7 @@ function saveCollisionRecordToVehicle(isEdit) {
}
function getAndValidateCollisionRecordValues() {
var collisionDate = $("#collisionRecordDate").val();
var collisionMileage = $("#collisionRecordMileage").val();
var collisionMileage = parseInt(globalParseFloat($("#collisionRecordMileage").val())).toString();
var collisionDescription = $("#collisionRecordDescription").val();
var collisionCost = $("#collisionRecordCost").val();
var collisionNotes = $("#collisionRecordNotes").val();

View File

@@ -28,4 +28,27 @@ function performLogOut() {
window.location.href = '/Login';
}
})
}
function loadPinnedNotes(vehicleId) {
var hoveredGrid = $(`#gridVehicle_${vehicleId}`);
if (hoveredGrid.attr("data-bs-title") == undefined) {
$.get(`/Vehicle/GetPinnedNotesByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data.length > 0) {
//converted pinned notes to html.
var htmlString = "<ul class='list-group list-group-flush'>";
data.forEach(x => {
htmlString += `<li><b>${x.description}</b> : ${x.noteText}</li>`;
});
htmlString += "</ul>";
hoveredGrid.attr("data-bs-title", htmlString);
new bootstrap.Tooltip(hoveredGrid);
hoveredGrid.tooltip("show");
}
});
} else {
hoveredGrid.tooltip("show");
}
}
function hidePinnedNotes(vehicleId) {
$(`#gridVehicle_${vehicleId}`).tooltip("hide");
}

View File

@@ -1,5 +1,5 @@
function showAddGasRecordModal() {
$.get('/Vehicle/GetAddGasRecordPartialView', function (data) {
$.get(`/Vehicle/GetAddGasRecordPartialView?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
if (data) {
$("#gasRecordModalContent").html(data);
//initiate datepicker
@@ -15,6 +15,11 @@ function showEditGasRecordModal(gasRecordId) {
//initiate datepicker
initDatePicker($('#gasRecordDate'));
$('#gasRecordModal').modal('show');
$('#gasRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("gasRecordNotes");
}
});
}
});
}
@@ -68,7 +73,7 @@ function saveGasRecordToVehicle(isEdit) {
}
function getAndValidateGasRecordValues() {
var gasDate = $("#gasRecordDate").val();
var gasMileage = $("#gasRecordMileage").val();
var gasMileage = parseInt(globalParseFloat($("#gasRecordMileage").val())).toString();
var gasGallons = $("#gasRecordGallons").val();
var gasCost = $("#gasRecordCost").val();
var gasCostType = $("#gasCostType").val();

View File

@@ -11,6 +11,11 @@ function showEditNoteModal(noteId) {
if (data) {
$("#noteModalContent").html(data);
$('#noteModal').modal('show');
$('#noteModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("noteTextArea");
}
});
}
});
}
@@ -67,6 +72,7 @@ function getAndValidateNoteValues() {
var noteText = $("#noteTextArea").val();
var vehicleId = GetVehicleId().vehicleId;
var noteId = getNoteModelData().id;
var noteIsPinned = $("#noteIsPinned").is(":checked");
//validation
var hasError = false;
if (noteDescription.trim() == '') { //eliminates whitespace.
@@ -86,6 +92,7 @@ function getAndValidateNoteValues() {
hasError: hasError,
vehicleId: vehicleId,
description: noteDescription,
noteText: noteText
noteText: noteText,
pinned: noteIsPinned
}
}

View File

@@ -15,6 +15,11 @@ function showEditOdometerRecordModal(odometerRecordId) {
//initiate datepicker
initDatePicker($('#odometerRecordDate'));
$('#odometerRecordModal').modal('show');
$('#odometerRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("odometerRecordNotes");
}
});
}
});
}
@@ -71,7 +76,7 @@ function saveOdometerRecordToVehicle(isEdit) {
}
function getAndValidateOdometerRecordValues() {
var serviceDate = $("#odometerRecordDate").val();
var serviceMileage = $("#odometerRecordMileage").val();
var serviceMileage = parseInt(globalParseFloat($("#odometerRecordMileage").val())).toString();
var serviceNotes = $("#odometerRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var odometerRecordId = getOdometerRecordModelData().id;

View File

@@ -15,6 +15,11 @@ function showEditPlanRecordModal(planRecordId) {
//initiate datepicker
initDatePicker($('#planRecordDate'));
$('#planRecordModal').modal('show');
$('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("planRecordNotes");
}
});
}
});
}

View File

@@ -4,6 +4,11 @@
$("#reminderRecordModalContent").html(data);
initDatePicker($('#reminderDate'), true);
$("#reminderRecordModal").modal("show");
$('#reminderRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("reminderNotes");
}
});
}
});
}
@@ -96,7 +101,7 @@ function markDoneReminderRecord(reminderRecordId, e) {
function getAndValidateReminderRecordValues() {
var reminderDate = $("#reminderDate").val();
var reminderMileage = $("#reminderMileage").val();
var reminderMileage = parseInt(globalParseFloat($("#reminderMileage").val())).toString();
var reminderDescription = $("#reminderDescription").val();
var reminderNotes = $("#reminderNotes").val();
var reminderOption = $('#reminderOptions input:radio:checked').val();

View File

@@ -74,4 +74,60 @@ function refreshCollaborators() {
$.get(`/Vehicle/GetCollaboratorsForVehicle?vehicleId=${vehicleId}`, function (data) {
$("#collaboratorContent").html(data);
});
}
function exportAttachments() {
Swal.fire({
title: 'Export Attachments',
html: `
<div id='attachmentTabs'>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportServiceRecord" class="form-check-input me-1" value='ServiceRecord'>
<label for="exportServiceRecord" class='form-check-label'>Service Record</label>
</div>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportRepairRecord" class="form-check-input me-1" value='RepairRecord'>
<label for="exportRepairRecord" class='form-check-label'>Repairs</label>
</div>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportUpgradeRecord" class="form-check-input me-1" value='UpgradeRecord'>
<label for="exportUpgradeRecord" class='form-check-label'>Upgrades</label>
</div>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportGasRecord" class="form-check-input me-1" value='GasRecord'>
<label for="exportGasRecord" class='form-check-label'>Fuel</label>
</div>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportTaxRecord" class="form-check-input me-1" value='TaxRecord'>
<label for="exportTaxRecord" class='form-check-label'>Taxes</label>
</div>
<div class='form-check form-check-inline'>
<input type="checkbox" id="exportOdometerRecord" class="form-check-input me-1" value='OdometerRecord'>
<label for="exportOdometerRecord" class='form-check-label'>Odometer</label>
</div>
</div>
`,
confirmButtonText: 'Export',
showCancelButton: true,
focusConfirm: false,
preConfirm: () => {
var selectedExportTabs = $("#attachmentTabs :checked").map(function () {
return this.value;
});
if (selectedExportTabs.toArray().length == 0) {
Swal.showValidationMessage(`Please make at least one selection`)
}
return { selectedTabs: selectedExportTabs.toArray() }
},
}).then(function (result) {
if (result.isConfirmed) {
var vehicleId = GetVehicleId().vehicleId;
$.post('/Vehicle/GetVehicleAttachments', { vehicleId: vehicleId, exportTabs: result.value.selectedTabs }, function (data) {
if (data.success) {
window.location.href = data.message;
} else {
errorToast(data.message);
}
})
}
});
}

View File

@@ -15,6 +15,11 @@ function showEditServiceRecordModal(serviceRecordId) {
//initiate datepicker
initDatePicker($('#serviceRecordDate'));
$('#serviceRecordModal').modal('show');
$('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("serviceRecordNotes");
}
});
}
});
}
@@ -71,7 +76,7 @@ function saveServiceRecordToVehicle(isEdit) {
}
function getAndValidateServiceRecordValues() {
var serviceDate = $("#serviceRecordDate").val();
var serviceMileage = $("#serviceRecordMileage").val();
var serviceMileage = parseInt(globalParseFloat($("#serviceRecordMileage").val())).toString();
var serviceDescription = $("#serviceRecordDescription").val();
var serviceCost = $("#serviceRecordCost").val();
var serviceNotes = $("#serviceRecordNotes").val();

View File

@@ -38,6 +38,7 @@ function saveVehicle(isEdit) {
var vehicleModel = $("#inputModel").val();
var vehicleLicensePlate = $("#inputLicensePlate").val();
var vehicleIsElectric = $("#inputIsElectric").is(":checked");
var vehicleUseHours = $("#inputUseHours").is(":checked");
//validate
var hasError = false;
if (vehicleYear.trim() == '' || parseInt(vehicleYear) < 1900) {
@@ -74,7 +75,8 @@ function saveVehicle(isEdit) {
make: vehicleMake,
model: vehicleModel,
licensePlate: vehicleLicensePlate,
isElectric: vehicleIsElectric
isElectric: vehicleIsElectric,
useHours: vehicleUseHours
}, function (data) {
if (data) {
if (!isEdit) {
@@ -108,6 +110,10 @@ function uploadFileAsync(event) {
if (response.trim() != '') {
uploadedFile = response;
}
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.")
}
});
}
@@ -154,4 +160,61 @@ function setDebounce(callBack) {
debounce = setTimeout(function () {
callBack();
}, 1000);
}
var storedTableRowState = null;
function toggleSort(tabName, sender) {
var sortColumn = sender.textContent;
var sortAscIcon = '<i class="bi bi-sort-numeric-down ms-2"></i>';
var sortDescIcon = '<i class="bi bi-sort-numeric-down-alt ms-2"></i>';
sender = $(sender);
//order of sort - asc, desc, reset
if (sender.hasClass('sort-asc')) {
sender.removeClass('sort-asc');
sender.addClass('sort-desc');
sender.html(`${sortColumn}${sortDescIcon}`);
sortTable(tabName, sortColumn, true);
} else if (sender.hasClass('sort-desc')) {
//restore table
sender.removeClass('sort-desc');
sender.html(`${sortColumn}`);
$(`#${tabName} table tbody`).html(storedTableRowState);
} else {
//first time sorting.
//check if table was sorted before by a different column(only relevant to fuel tab)
if (storedTableRowState != null && ($(".sort-asc").length > 0 || $(".sort-desc").length > 0)) {
//restore table state.
$(`#${tabName} table tbody`).html(storedTableRowState);
//reset other sorted columns
if ($(".sort-asc").length > 0) {
$(".sort-asc").html($(".sort-asc").html().replace(sortAscIcon, ""));
$(".sort-asc").removeClass("sort-asc");
}
if ($(".sort-desc").length > 0) {
$(".sort-desc").html($(".sort-desc").html().replace(sortDescIcon, ""));
$(".sort-desc").removeClass("sort-desc");
}
}
sender.addClass('sort-asc');
sender.html(`${sortColumn}${sortAscIcon}`);
storedTableRowState = null;
storedTableRowState = $(`#${tabName} table tbody`).html();
sortTable(tabName, sortColumn, false);
}
}
function sortTable(tabName, columnName, desc) {
//get column index.
var columns = $(`#${tabName} table th`).toArray().map(x => x.innerText);
var colIndex = columns.findIndex(x => x == columnName);
//get row data
var rowData = $(`#${tabName} table tbody tr`);
var sortedRow = rowData.toArray().sort((a, b) => {
var currentVal = globalParseFloat(a.children[colIndex].textContent);
var nextVal = globalParseFloat(b.children[colIndex].textContent);
if (desc) {
return nextVal - currentVal;
} else {
return currentVal - nextVal;
}
});
$(`#${tabName} table tbody`).html(sortedRow);
}

View File

@@ -15,6 +15,11 @@ function showEditSupplyRecordModal(supplyRecordId) {
//initiate datepicker
initDatePicker($('#supplyRecordDate'));
$('#supplyRecordModal').modal('show');
$('#supplyRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("supplyRecordNotes");
}
});
}
});
}

View File

@@ -15,9 +15,22 @@ function showEditTaxRecordModal(taxRecordId) {
//initiate datepicker
initDatePicker($('#taxRecordDate'));
$('#taxRecordModal').modal('show');
$('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("taxRecordNotes");
}
});
}
});
}
function enableTaxRecurring() {
var taxIsRecurring = $("#taxIsRecurring").is(":checked");
if (taxIsRecurring) {
$("#taxRecurringMonth").attr('disabled', false);
} else {
$("#taxRecurringMonth").attr('disabled', true);
}
}
function hideAddTaxRecordModal() {
$('#taxRecordModal').modal('hide');
}
@@ -76,6 +89,8 @@ function getAndValidateTaxRecordValues() {
var taxNotes = $("#taxRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var taxRecordId = getTaxRecordModelData().id;
var taxIsRecurring = $("#taxIsRecurring").is(":checked");
var taxRecurringMonth = $("#taxRecurringMonth").val();
var addReminderRecord = $("#addReminderCheck").is(":checked");
//validation
var hasError = false;
@@ -105,6 +120,8 @@ function getAndValidateTaxRecordValues() {
description: taxDescription,
cost: taxCost,
notes: taxNotes,
isRecurring: taxIsRecurring,
recurringInterval: taxRecurringMonth,
files: uploadedFiles,
addReminderRecord: addReminderRecord
}

View File

@@ -15,6 +15,11 @@ function showEditUpgradeRecordModal(upgradeRecordId) {
//initiate datepicker
initDatePicker($('#upgradeRecordDate'));
$('#upgradeRecordModal').modal('show');
$('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("upgradeRecordNotes");
}
});
}
});
}
@@ -71,7 +76,7 @@ function saveUpgradeRecordToVehicle(isEdit) {
}
function getAndValidateUpgradeRecordValues() {
var upgradeDate = $("#upgradeRecordDate").val();
var upgradeMileage = $("#upgradeRecordMileage").val();
var upgradeMileage = parseInt(globalParseFloat($("#upgradeRecordMileage").val())).toString();
var upgradeDescription = $("#upgradeRecordDescription").val();
var upgradeCost = $("#upgradeRecordCost").val();
var upgradeNotes = $("#upgradeRecordNotes").val();

View File

@@ -278,6 +278,10 @@ function uploadVehicleFilesAsync(event) {
if (response.length > 0) {
uploadedFiles.push.apply(uploadedFiles, response);
}
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.")
}
});
}
@@ -313,7 +317,11 @@ function getVehicleHaveImportantReminders(vehicleId) {
});
}, 500);
}
function printTab() {
setTimeout(function () {
window.print();
}, 500);
}
function deleteFileFromUploadedFiles(fileLocation, event) {
event.parentElement.parentElement.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
@@ -349,4 +357,82 @@ function saveScrollPosition() {
function restoreScrollPosition() {
$(".vehicleDetailTabContainer").scrollTop(scrollPosition);
scrollPosition = 0;
}
function moveRecord(recordId, source, dest) {
$("#workAroundInput").show();
var friendlySource = "";
var friendlyDest = "";
var hideModalCallBack;
var refreshDataCallBack;
switch (source) {
case "ServiceRecord":
friendlySource = "Service Records";
hideModalCallBack = hideAddServiceRecordModal;
refreshDataCallBack = getVehicleServiceRecords;
break;
case "RepairRecord":
friendlySource = "Repairs";
hideModalCallBack = hideAddCollisionRecordModal;
refreshDataCallBack = getVehicleCollisionRecords;
break;
case "UpgradeRecord":
friendlySource = "Upgrades";
hideModalCallBack = hideAddUpgradeRecordModal;
refreshDataCallBack = getVehicleUpgradeRecords;
break;
}
switch (dest) {
case "ServiceRecord":
friendlyDest = "Service Records";
break;
case "RepairRecord":
friendlyDest = "Repairs";
break;
case "UpgradeRecord":
friendlyDest = "Upgrades";
break;
}
Swal.fire({
title: "Confirm Move?",
text: `Move this record from ${friendlySource} to ${friendlyDest}?`,
showCancelButton: true,
confirmButtonText: "Move",
confirmButtonColor: "#dc3545"
}).then((result) => {
if (result.isConfirmed) {
$.post('/Vehicle/MoveRecord', {recordId: recordId, source: source, destination: dest }, function (data) {
if (data) {
hideModalCallBack();
successToast("Record Moved");
var vehicleId = GetVehicleId().vehicleId;
refreshDataCallBack(vehicleId);
} else {
errorToast("An error has occurred, please try again later.");
}
});
} else {
$("#workAroundInput").hide();
}
});
}
function toggleMarkDownOverlay(textAreaName) {
var textArea = $(`#${textAreaName}`);
if ($(".markdown-overlay").length > 0) {
$(".markdown-overlay").remove();
return;
}
var text = textArea.val();
if (text == undefined) {
return;
}
if (text.length > 0) {
var formatted = markdown(text);
//var overlay div
var overlayDiv = `<div class='markdown-overlay' style="z-index: 1060; position:absolute; top:${textArea.css('top')}; left:${textArea.css('left')}; width:${textArea.css('width')}; height:${textArea.css('height')}; padding:${textArea.css('padding')}; overflow-y:auto; background-color:var(--bs-modal-bg);">${formatted}</div>`;
textArea.parent().children(`label[for=${textAreaName}]`).append(overlayDiv);
}
}
function showLinks(e) {
var textAreaName = $(e.parentElement).attr("for");
toggleMarkDownOverlay(textAreaName);
}

View File

@@ -0,0 +1,129 @@
/**
* drawdown.js
* (c) Adam Leggett
*/
;function markdown(src) {
var rx_lt = /</g;
var rx_gt = />/g;
var rx_space = /\t|\r|\uf8ff/g;
var rx_escape = /\\([\\\|`*_{}\[\]()#+\-~])/g;
var rx_hr = /^([*\-=_] *){3,}$/gm;
var rx_blockquote = /\n *&gt; *([^]*?)(?=(\n|$){2})/g;
var rx_list = /\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g;
var rx_listjoin = /<\/(ol|ul)>\n\n<\1>/g;
var rx_highlight = /(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+)|`)(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g;
var rx_code = /\n((```|~~~).*\n?([^]*?)\n?\2|(( .*?\n)+))/g;
var rx_link = /((!?)\[(.*?)\]\((.*?)( ".*")?\)|\\([\\`*_{}\[\]()#+\-.!~]))/g;
var rx_table = /\n(( *\|.*?\| *\n)+)/g;
var rx_thead = /^.*\n( *\|( *\:?-+\:?-+\:? *\|)* *\n|)/;
var rx_row = /.*\n/g;
var rx_cell = /\||(.*?[^\\])\|/g;
var rx_heading = /(?=^|>|\n)([>\s]*?)(#{1,6}) (.*?)( #*)? *(?=\n|$)/g;
var rx_para = /(?=^|>|\n)\s*\n+([^<]+?)\n+\s*(?=\n|<|$)/g;
var rx_stash = /-\d+\uf8ff/g;
function replace(rex, fn) {
src = src.replace(rex, fn);
}
function element(tag, content) {
return '<' + tag + '>' + content + '</' + tag + '>';
}
function blockquote(src) {
return src.replace(rx_blockquote, function(all, content) {
return element('blockquote', blockquote(highlight(content.replace(/^ *&gt; */gm, ''))));
});
}
function list(src) {
return src.replace(rx_list, function(all, ind, ol, num, low, content) {
var entry = element('li', highlight(content.split(
RegExp('\n ?' + ind + '(?:(?:\\d+|[a-zA-Z])[.)]|[*\\-+]) +', 'g')).map(list).join('</li><li>')));
return '\n' + (ol
? '<ol start="' + (num
? ol + '">'
: parseInt(ol,36) - 9 + '" style="list-style-type:' + (low ? 'low' : 'upp') + 'er-alpha">') + entry + '</ol>'
: element('ul', entry));
});
}
function highlight(src) {
return src.replace(rx_highlight, function(all, _, p1, emp, sub, sup, small, big, p2, content) {
return _ + element(
emp ? (p2 ? 'strong' : 'em')
: sub ? (p2 ? 's' : 'sub')
: sup ? 'sup'
: small ? 'small'
: big ? 'big'
: 'code',
highlight(content));
});
}
function unesc(str) {
return str.replace(rx_escape, '$1');
}
var stash = [];
var si = 0;
src = '\n' + src + '\n';
replace(rx_lt, '&lt;');
replace(rx_gt, '&gt;');
replace(rx_space, ' ');
// blockquote
src = blockquote(src);
// horizontal rule
replace(rx_hr, '<hr/>');
// list
src = list(src);
replace(rx_listjoin, '');
// code
replace(rx_code, function(all, p1, p2, p3, p4) {
stash[--si] = element('pre', element('code', p3||p4.replace(/^ /gm, '')));
return si + '\uf8ff';
});
// link or image
replace(rx_link, function(all, p1, p2, p3, p4, p5, p6) {
stash[--si] = p4
? p2
? '<img src="' + p4 + '" alt="' + p3 + '"/>'
: '<a href="' + p4 + '">' + unesc(highlight(p3)) + '</a>'
: p6;
return si + '\uf8ff';
});
// table
replace(rx_table, function(all, table) {
var sep = table.match(rx_thead)[1];
return '\n' + element('table',
table.replace(rx_row, function(row, ri) {
return row == sep ? '' : element('tr', row.replace(rx_cell, function(all, cell, ci) {
return ci ? element(sep && !ri ? 'th' : 'td', unesc(highlight(cell || ''))) : ''
}))
})
)
});
// heading
replace(rx_heading, function(all, _, p1, p2) { return _ + element('h' + p1.length, unesc(highlight(p2))) });
// paragraph
replace(rx_para, function(all, content) { return element('p', unesc(highlight(content))) });
// stash
replace(rx_stash, function(all) { return stash[parseInt(all)] });
return src.trim();
};