Compare commits
123 Commits
Hargata/to
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bdd3d902b | ||
|
|
abb44608fe | ||
|
|
0b1e3f4be8 | ||
|
|
156eb8ec27 | ||
|
|
472dcac590 | ||
|
|
b4d86f415c | ||
|
|
812e768f93 | ||
|
|
a08d4798e1 | ||
|
|
7bc08c9950 | ||
|
|
20107e3f05 | ||
|
|
ed678c36a5 | ||
|
|
f7574a9a60 | ||
|
|
e506b543e6 | ||
|
|
019a549b64 | ||
|
|
8dbc883a8c | ||
|
|
5fd918115c | ||
|
|
a11f8377fe | ||
|
|
7be19f8603 | ||
|
|
5407ebecb1 | ||
|
|
b75a1b4e6c | ||
|
|
1c28039aaf | ||
|
|
3a1836ef84 | ||
|
|
c747889f85 | ||
|
|
338a8426b2 | ||
|
|
4b1701d158 | ||
|
|
5d64a9a422 | ||
|
|
c2315db127 | ||
|
|
e40129f701 | ||
|
|
d198ad9a6b | ||
|
|
abffc5ab60 | ||
|
|
24ff9db32d | ||
|
|
07fa560344 | ||
|
|
28615d9038 | ||
|
|
d8f53e4b65 | ||
|
|
1f0cef4161 | ||
|
|
af5d72e0d0 | ||
|
|
1397e92709 | ||
|
|
a49121b1d9 | ||
|
|
ca65a6fb71 | ||
|
|
a54857c6bf | ||
|
|
2eadd05289 | ||
|
|
6eafa5e036 | ||
|
|
40f01ad100 | ||
|
|
161b36325e | ||
|
|
f9a6ddd513 | ||
|
|
e85110f8b6 | ||
|
|
76d3dba574 | ||
|
|
178b50a033 | ||
|
|
fb45aefde3 | ||
|
|
05bbc8a95b | ||
|
|
f123299dd6 | ||
|
|
7174413e2a | ||
|
|
d816f73598 | ||
|
|
b249ccdd6c | ||
|
|
4a007530a6 | ||
|
|
04ce448b23 | ||
|
|
ccd446f299 | ||
|
|
c49c8a5301 | ||
|
|
55cc2819d0 | ||
|
|
f8de7de0d6 | ||
|
|
2ea1bc2c20 | ||
|
|
90d095ea51 | ||
|
|
e1d12d0918 | ||
|
|
d9d0957040 | ||
|
|
9d73db3c51 | ||
|
|
c0f0786fd4 | ||
|
|
357eff116f | ||
|
|
32a047c522 | ||
|
|
8a237bb7ec | ||
|
|
f5b9072cc6 | ||
|
|
4e3eaa53ff | ||
|
|
4779d3f161 | ||
|
|
b0173fae94 | ||
|
|
c6ee8830a3 | ||
|
|
c2eeab5025 | ||
|
|
43fd40347f | ||
|
|
53139f9bb2 | ||
|
|
0b6033cc00 | ||
|
|
6f0115e5c5 | ||
|
|
2403adf537 | ||
|
|
f00ab897b5 | ||
|
|
093631bf6f | ||
|
|
5c2835ab76 | ||
|
|
03029981fd | ||
|
|
69b1838038 | ||
|
|
1c2f83026c | ||
|
|
d36f2c59e3 | ||
|
|
53ebec3f03 | ||
|
|
f2cbbaeb12 | ||
|
|
31c1202649 | ||
|
|
331499461a | ||
|
|
8c6dd5e343 | ||
|
|
01b1e8228d | ||
|
|
43979d6115 | ||
|
|
6a9860e202 | ||
|
|
39338e8028 | ||
|
|
d1d5351a01 | ||
|
|
c9385c7fdd | ||
|
|
afaae89af6 | ||
|
|
b9d799cd49 | ||
|
|
e47c541e08 | ||
|
|
51ff01d2cd | ||
|
|
618399cb09 | ||
|
|
b837a2e528 | ||
|
|
a1e8b8f9cc | ||
|
|
787c5da72a | ||
|
|
260703be8e | ||
|
|
053801b046 | ||
|
|
db9b1970c5 | ||
|
|
b153ef5ea5 | ||
|
|
0d1c7234e8 | ||
|
|
e3abe5f209 | ||
|
|
b453bfce5b | ||
|
|
147a1b03a7 | ||
|
|
3017db5f86 | ||
|
|
399d0d8058 | ||
|
|
b8c0d4ef67 | ||
|
|
918d086705 | ||
|
|
ac05acf96b | ||
|
|
e7449806c0 | ||
|
|
baab3213b5 | ||
|
|
d68e9e9589 | ||
|
|
dbb139dfad |
3
.env
3
.env
@@ -5,4 +5,5 @@ MailConfig__EmailFrom=""
|
|||||||
MailConfig__UseSSL="false"
|
MailConfig__UseSSL="false"
|
||||||
MailConfig__Port=587
|
MailConfig__Port=587
|
||||||
MailConfig__Username=""
|
MailConfig__Username=""
|
||||||
MailConfig__Password=""
|
MailConfig__Password=""
|
||||||
|
LOGGING__LOGLEVEL__DEFAULT=Error
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
name: Docker Image To GHCR
|
name: Build and Push Image to Dockerhub and GHCR
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: "${{ secrets.DH_USER }}"
|
||||||
|
password: "${{ secrets.DH_PASS }}"
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -28,4 +30,6 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
hargata/lubelogger:latest
|
||||||
ghcr.io/hargata/lubelogger:latest
|
ghcr.io/hargata/lubelogger:latest
|
||||||
|
|
||||||
30
.github/workflows/dockerhub-docker-image.yml
vendored
30
.github/workflows/dockerhub-docker-image.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Docker Image To Docker Hub
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: "${{ secrets.DH_USER }}"
|
|
||||||
password: "${{ secrets.DH_PASS }}"
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
hargata/lubelogger:latest
|
|
||||||
@@ -22,10 +22,13 @@ namespace CarCareTracker.Controllers
|
|||||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||||
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
|
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
|
||||||
|
private readonly IUserAccessDataAccess _userAccessDataAccess;
|
||||||
|
private readonly IUserRecordDataAccess _userRecordDataAccess;
|
||||||
private readonly IReminderHelper _reminderHelper;
|
private readonly IReminderHelper _reminderHelper;
|
||||||
private readonly IGasHelper _gasHelper;
|
private readonly IGasHelper _gasHelper;
|
||||||
private readonly IUserLogic _userLogic;
|
private readonly IUserLogic _userLogic;
|
||||||
private readonly IFileHelper _fileHelper;
|
private readonly IFileHelper _fileHelper;
|
||||||
|
private readonly IMailHelper _mailHelper;
|
||||||
public APIController(IVehicleDataAccess dataAccess,
|
public APIController(IVehicleDataAccess dataAccess,
|
||||||
IGasHelper gasHelper,
|
IGasHelper gasHelper,
|
||||||
IReminderHelper reminderHelper,
|
IReminderHelper reminderHelper,
|
||||||
@@ -37,6 +40,9 @@ namespace CarCareTracker.Controllers
|
|||||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||||
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
||||||
IOdometerRecordDataAccess odometerRecordDataAccess,
|
IOdometerRecordDataAccess odometerRecordDataAccess,
|
||||||
|
IUserAccessDataAccess userAccessDataAccess,
|
||||||
|
IUserRecordDataAccess userRecordDataAccess,
|
||||||
|
IMailHelper mailHelper,
|
||||||
IFileHelper fileHelper,
|
IFileHelper fileHelper,
|
||||||
IUserLogic userLogic)
|
IUserLogic userLogic)
|
||||||
{
|
{
|
||||||
@@ -49,6 +55,9 @@ namespace CarCareTracker.Controllers
|
|||||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||||
_odometerRecordDataAccess = odometerRecordDataAccess;
|
_odometerRecordDataAccess = odometerRecordDataAccess;
|
||||||
|
_userAccessDataAccess = userAccessDataAccess;
|
||||||
|
_userRecordDataAccess = userRecordDataAccess;
|
||||||
|
_mailHelper = mailHelper;
|
||||||
_gasHelper = gasHelper;
|
_gasHelper = gasHelper;
|
||||||
_reminderHelper = reminderHelper;
|
_reminderHelper = reminderHelper;
|
||||||
_userLogic = userLogic;
|
_userLogic = userLogic;
|
||||||
@@ -428,12 +437,65 @@ namespace CarCareTracker.Controllers
|
|||||||
}
|
}
|
||||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||||
[HttpGet]
|
[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")]
|
[Route("/api/makebackup")]
|
||||||
public IActionResult MakeBackup()
|
public IActionResult MakeBackup()
|
||||||
{
|
{
|
||||||
var result = _fileHelper.MakeBackup();
|
var result = _fileHelper.MakeBackup();
|
||||||
return Json(result);
|
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)
|
private int GetMaxMileage(int vehicleId)
|
||||||
{
|
{
|
||||||
var numbersArray = new List<int>();
|
var numbersArray = new List<int>();
|
||||||
|
|||||||
@@ -317,7 +317,6 @@ namespace CarCareTracker.Controllers
|
|||||||
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||||
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
||||||
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
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 convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
|
||||||
var exportData = convertedRecords.Select(x => new GasRecordExportModel
|
var exportData = convertedRecords.Select(x => new GasRecordExportModel
|
||||||
{
|
{
|
||||||
@@ -377,7 +376,7 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
VehicleId = vehicleId,
|
VehicleId = vehicleId,
|
||||||
Date = DateTime.Parse(importModel.Date),
|
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),
|
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
|
||||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
|
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
|
||||||
};
|
};
|
||||||
@@ -421,7 +420,7 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
VehicleId = vehicleId,
|
VehicleId = vehicleId,
|
||||||
Date = DateTime.Parse(importModel.Date),
|
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,
|
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
|
||||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||||
@@ -434,7 +433,7 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
VehicleId = vehicleId,
|
VehicleId = vehicleId,
|
||||||
Date = DateTime.Parse(importModel.Date),
|
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
|
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
|
||||||
};
|
};
|
||||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
|
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
|
||||||
@@ -464,7 +463,7 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
VehicleId = vehicleId,
|
VehicleId = vehicleId,
|
||||||
Date = DateTime.Parse(importModel.Date),
|
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,
|
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
|
||||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||||
@@ -477,7 +476,7 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
VehicleId = vehicleId,
|
VehicleId = vehicleId,
|
||||||
Date = DateTime.Parse(importModel.Date),
|
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,
|
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
|
||||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||||
@@ -530,8 +529,6 @@ namespace CarCareTracker.Controllers
|
|||||||
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
|
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
|
||||||
{
|
{
|
||||||
var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(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.
|
//check if the user uses MPG or Liters per 100km.
|
||||||
var userConfig = _config.GetUserConfig(User);
|
var userConfig = _config.GetUserConfig(User);
|
||||||
bool useMPG = userConfig.UseMPG;
|
bool useMPG = userConfig.UseMPG;
|
||||||
@@ -541,10 +538,13 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
|
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()
|
var viewModel = new GasRecordViewModelContainer()
|
||||||
{
|
{
|
||||||
UseKwh = vehicleIsElectric,
|
UseKwh = vehicleIsElectric,
|
||||||
|
UseHours = vehicleUseHours,
|
||||||
GasRecords = computedResults
|
GasRecords = computedResults
|
||||||
};
|
};
|
||||||
return PartialView("_Gas", viewModel);
|
return PartialView("_Gas", viewModel);
|
||||||
@@ -567,9 +567,12 @@ namespace CarCareTracker.Controllers
|
|||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
[HttpGet]
|
[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]
|
[HttpGet]
|
||||||
public IActionResult GetGasRecordForEditById(int gasRecordId)
|
public IActionResult GetGasRecordForEditById(int gasRecordId)
|
||||||
@@ -588,10 +591,13 @@ namespace CarCareTracker.Controllers
|
|||||||
MissedFuelUp = result.MissedFuelUp,
|
MissedFuelUp = result.MissedFuelUp,
|
||||||
Notes = result.Notes
|
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()
|
var viewModel = new GasRecordInputContainer()
|
||||||
{
|
{
|
||||||
UseKwh = vehicleIsElectric,
|
UseKwh = vehicleIsElectric,
|
||||||
|
UseHours = vehicleUseHours,
|
||||||
GasRecord = convertedResult
|
GasRecord = convertedResult
|
||||||
};
|
};
|
||||||
return PartialView("_GasModal", viewModel);
|
return PartialView("_GasModal", viewModel);
|
||||||
@@ -602,6 +608,15 @@ namespace CarCareTracker.Controllers
|
|||||||
var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId);
|
var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId);
|
||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit)
|
||||||
|
{
|
||||||
|
var currentConfig = _config.GetUserConfig(User);
|
||||||
|
currentConfig.PreferredGasUnit = gasUnit;
|
||||||
|
currentConfig.PreferredGasMileageUnit = fuelMileageUnit;
|
||||||
|
var result = _config.SaveUserConfig(User, currentConfig);
|
||||||
|
return Json(result);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
#region "Service Records"
|
#region "Service Records"
|
||||||
[TypeFilter(typeof(CollaboratorFilter))]
|
[TypeFilter(typeof(CollaboratorFilter))]
|
||||||
@@ -661,7 +676,8 @@ namespace CarCareTracker.Controllers
|
|||||||
Mileage = result.Mileage,
|
Mileage = result.Mileage,
|
||||||
Notes = result.Notes,
|
Notes = result.Notes,
|
||||||
VehicleId = result.VehicleId,
|
VehicleId = result.VehicleId,
|
||||||
Files = result.Files
|
Files = result.Files,
|
||||||
|
Tags = result.Tags
|
||||||
};
|
};
|
||||||
return PartialView("_ServiceRecordModal", convertedResult);
|
return PartialView("_ServiceRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -730,7 +746,8 @@ namespace CarCareTracker.Controllers
|
|||||||
Mileage = result.Mileage,
|
Mileage = result.Mileage,
|
||||||
Notes = result.Notes,
|
Notes = result.Notes,
|
||||||
VehicleId = result.VehicleId,
|
VehicleId = result.VehicleId,
|
||||||
Files = result.Files
|
Files = result.Files,
|
||||||
|
Tags = result.Tags
|
||||||
};
|
};
|
||||||
return PartialView("_CollisionRecordModal", convertedResult);
|
return PartialView("_CollisionRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -778,7 +795,8 @@ namespace CarCareTracker.Controllers
|
|||||||
IsRecurring = true,
|
IsRecurring = true,
|
||||||
Notes = recurringFee.Notes,
|
Notes = recurringFee.Notes,
|
||||||
RecurringInterval = recurringFee.RecurringInterval,
|
RecurringInterval = recurringFee.RecurringInterval,
|
||||||
Files = recurringFee.Files
|
Files = recurringFee.Files,
|
||||||
|
Tags = recurringFee.Tags
|
||||||
};
|
};
|
||||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
|
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
|
||||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
|
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
|
||||||
@@ -814,7 +832,8 @@ namespace CarCareTracker.Controllers
|
|||||||
VehicleId = result.VehicleId,
|
VehicleId = result.VehicleId,
|
||||||
IsRecurring = result.IsRecurring,
|
IsRecurring = result.IsRecurring,
|
||||||
RecurringInterval = result.RecurringInterval,
|
RecurringInterval = result.RecurringInterval,
|
||||||
Files = result.Files
|
Files = result.Files,
|
||||||
|
Tags = result.Tags
|
||||||
};
|
};
|
||||||
return PartialView("_TaxRecordModal", convertedResult);
|
return PartialView("_TaxRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -975,6 +994,84 @@ namespace CarCareTracker.Controllers
|
|||||||
return PartialView("_ReminderMakeUpReport", viewModel);
|
return PartialView("_ReminderMakeUpReport", viewModel);
|
||||||
}
|
}
|
||||||
[TypeFilter(typeof(CollaboratorFilter))]
|
[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)
|
public IActionResult GetVehicleHistory(int vehicleId)
|
||||||
{
|
{
|
||||||
var vehicleHistory = new VehicleHistoryViewModel();
|
var vehicleHistory = new VehicleHistoryViewModel();
|
||||||
@@ -988,15 +1085,28 @@ namespace CarCareTracker.Controllers
|
|||||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||||
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
||||||
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
||||||
|
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
|
||||||
vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost);
|
vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost);
|
||||||
vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost);
|
vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost);
|
||||||
var averageMPG = "0";
|
var averageMPG = "0";
|
||||||
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
|
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
|
||||||
if (gasViewModels.Any())
|
if (gasViewModels.Any())
|
||||||
{
|
{
|
||||||
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels);
|
averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG);
|
||||||
}
|
}
|
||||||
vehicleHistory.MPG = averageMPG;
|
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleHistory.VehicleData.IsElectric, vehicleHistory.VehicleData.UseHours, useMPG, useUKMPG);
|
||||||
|
if (fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l")
|
||||||
|
{
|
||||||
|
//conversion needed.
|
||||||
|
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
|
||||||
|
if (newAverageMPG != 0)
|
||||||
|
{
|
||||||
|
newAverageMPG = 100 / newAverageMPG;
|
||||||
|
}
|
||||||
|
averageMPG = newAverageMPG.ToString("F");
|
||||||
|
fuelEconomyMileageUnit = preferredFuelMileageUnit;
|
||||||
|
}
|
||||||
|
vehicleHistory.MPG = $"{averageMPG} {fuelEconomyMileageUnit}";
|
||||||
//insert servicerecords
|
//insert servicerecords
|
||||||
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
|
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
|
||||||
{
|
{
|
||||||
@@ -1225,7 +1335,8 @@ namespace CarCareTracker.Controllers
|
|||||||
Metric = result.Metric,
|
Metric = result.Metric,
|
||||||
IsRecurring = result.IsRecurring,
|
IsRecurring = result.IsRecurring,
|
||||||
ReminderMileageInterval = result.ReminderMileageInterval,
|
ReminderMileageInterval = result.ReminderMileageInterval,
|
||||||
ReminderMonthInterval = result.ReminderMonthInterval
|
ReminderMonthInterval = result.ReminderMonthInterval,
|
||||||
|
CustomMileageInterval = result.CustomMileageInterval
|
||||||
};
|
};
|
||||||
return PartialView("_ReminderRecordModal", convertedResult);
|
return PartialView("_ReminderRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -1294,7 +1405,8 @@ namespace CarCareTracker.Controllers
|
|||||||
Mileage = result.Mileage,
|
Mileage = result.Mileage,
|
||||||
Notes = result.Notes,
|
Notes = result.Notes,
|
||||||
VehicleId = result.VehicleId,
|
VehicleId = result.VehicleId,
|
||||||
Files = result.Files
|
Files = result.Files,
|
||||||
|
Tags = result.Tags
|
||||||
};
|
};
|
||||||
return PartialView("_UpgradeRecordModal", convertedResult);
|
return PartialView("_UpgradeRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -1314,6 +1426,14 @@ namespace CarCareTracker.Controllers
|
|||||||
result = result.OrderByDescending(x => x.Pinned).ToList();
|
result = result.OrderByDescending(x => x.Pinned).ToList();
|
||||||
return PartialView("_Notes", result);
|
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]
|
[HttpPost]
|
||||||
public IActionResult SaveNoteToVehicleId(Note note)
|
public IActionResult SaveNoteToVehicleId(Note note)
|
||||||
{
|
{
|
||||||
@@ -1593,7 +1713,8 @@ namespace CarCareTracker.Controllers
|
|||||||
Mileage = result.Mileage,
|
Mileage = result.Mileage,
|
||||||
Notes = result.Notes,
|
Notes = result.Notes,
|
||||||
VehicleId = result.VehicleId,
|
VehicleId = result.VehicleId,
|
||||||
Files = result.Files
|
Files = result.Files,
|
||||||
|
Tags = result.Tags
|
||||||
};
|
};
|
||||||
return PartialView("_OdometerRecordModal", convertedResult);
|
return PartialView("_OdometerRecordModal", convertedResult);
|
||||||
}
|
}
|
||||||
@@ -1604,5 +1725,55 @@ namespace CarCareTracker.Controllers
|
|||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
#endregion
|
#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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
public enum ReminderMileageInterval
|
public enum ReminderMileageInterval
|
||||||
{
|
{
|
||||||
|
Other = 0,
|
||||||
FiftyMiles = 50,
|
FiftyMiles = 50,
|
||||||
OneHundredMiles = 100,
|
OneHundredMiles = 100,
|
||||||
FiveHundredMiles = 500,
|
FiveHundredMiles = 500,
|
||||||
@@ -14,7 +15,9 @@
|
|||||||
FifteenThousandMiles = 15000,
|
FifteenThousandMiles = 15000,
|
||||||
TwentyThousandMiles = 20000,
|
TwentyThousandMiles = 20000,
|
||||||
ThirtyThousandMiles = 30000,
|
ThirtyThousandMiles = 30000,
|
||||||
|
FortyThousandMiles = 40000,
|
||||||
FiftyThousandMiles = 50000,
|
FiftyThousandMiles = 50000,
|
||||||
|
SixtyThousandMiles = 60000,
|
||||||
OneHundredThousandMiles = 100000,
|
OneHundredThousandMiles = 100000,
|
||||||
OneHundredFiftyThousandMiles = 150000
|
OneHundredFiftyThousandMiles = 150000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace CarCareTracker.Helper
|
|||||||
{
|
{
|
||||||
UserConfig GetUserConfig(ClaimsPrincipal user);
|
UserConfig GetUserConfig(ClaimsPrincipal user);
|
||||||
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
|
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
|
||||||
|
string GetLogoUrl();
|
||||||
public bool DeleteUserConfig(int userId);
|
public bool DeleteUserConfig(int userId);
|
||||||
}
|
}
|
||||||
public class ConfigHelper : IConfigHelper
|
public class ConfigHelper : IConfigHelper
|
||||||
@@ -24,6 +25,15 @@ namespace CarCareTracker.Helper
|
|||||||
_userConfig = userConfig;
|
_userConfig = userConfig;
|
||||||
_cache = memoryCache;
|
_cache = memoryCache;
|
||||||
}
|
}
|
||||||
|
public string GetLogoUrl()
|
||||||
|
{
|
||||||
|
var logoUrl = _config["LUBELOGGER_LOGO_URL"];
|
||||||
|
if (string.IsNullOrWhiteSpace(logoUrl))
|
||||||
|
{
|
||||||
|
logoUrl = "/defaults/lubelogger_logo.png";
|
||||||
|
}
|
||||||
|
return logoUrl;
|
||||||
|
}
|
||||||
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
|
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
|
||||||
{
|
{
|
||||||
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
@@ -94,9 +104,12 @@ namespace CarCareTracker.Helper
|
|||||||
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
|
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
|
||||||
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
|
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
|
||||||
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
|
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
|
||||||
|
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
|
||||||
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
|
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
|
||||||
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
|
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
|
||||||
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
|
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
|
||||||
|
PreferredGasMileageUnit = _config[nameof(UserConfig.PreferredGasMileageUnit)],
|
||||||
|
PreferredGasUnit = _config[nameof(UserConfig.PreferredGasUnit)],
|
||||||
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
|
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
|
||||||
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
|
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.IO.Compression;
|
using CarCareTracker.Models;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace CarCareTracker.Helper
|
namespace CarCareTracker.Helper
|
||||||
{
|
{
|
||||||
@@ -8,7 +9,8 @@ namespace CarCareTracker.Helper
|
|||||||
string MoveFileFromTemp(string currentFilePath, string newFolder);
|
string MoveFileFromTemp(string currentFilePath, string newFolder);
|
||||||
bool DeleteFile(string currentFilePath);
|
bool DeleteFile(string currentFilePath);
|
||||||
string MakeBackup();
|
string MakeBackup();
|
||||||
bool RestoreBackup(string fileName);
|
bool RestoreBackup(string fileName, bool clearExisting = false);
|
||||||
|
string MakeAttachmentsExport(List<GenericReportModel> exportData);
|
||||||
}
|
}
|
||||||
public class FileHelper : IFileHelper
|
public class FileHelper : IFileHelper
|
||||||
{
|
{
|
||||||
@@ -38,7 +40,7 @@ namespace CarCareTracker.Helper
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool RestoreBackup(string fileName)
|
public bool RestoreBackup(string fileName, bool clearExisting = false)
|
||||||
{
|
{
|
||||||
var fullFilePath = GetFullFilePath(fileName);
|
var fullFilePath = GetFullFilePath(fileName);
|
||||||
if (string.IsNullOrWhiteSpace(fullFilePath))
|
if (string.IsNullOrWhiteSpace(fullFilePath))
|
||||||
@@ -64,9 +66,17 @@ namespace CarCareTracker.Helper
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(existingPath);
|
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
|
//copy each files from temp folder to newPath
|
||||||
var filesToUpload = Directory.GetFiles(imagePath);
|
var filesToUpload = Directory.GetFiles(imagePath);
|
||||||
foreach(string file in filesToUpload)
|
foreach (string file in filesToUpload)
|
||||||
{
|
{
|
||||||
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
|
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
|
||||||
}
|
}
|
||||||
@@ -78,6 +88,14 @@ namespace CarCareTracker.Helper
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(existingPath);
|
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
|
//copy each files from temp folder to newPath
|
||||||
var filesToUpload = Directory.GetFiles(documentPath);
|
var filesToUpload = Directory.GetFiles(documentPath);
|
||||||
foreach (string file in filesToUpload)
|
foreach (string file in filesToUpload)
|
||||||
@@ -100,12 +118,37 @@ namespace CarCareTracker.Helper
|
|||||||
File.Move(configPath, StaticHelper.UserConfigPath, true);
|
File.Move(configPath, StaticHelper.UserConfigPath, true);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception ex)
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
|
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
|
||||||
return false;
|
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()
|
public string MakeBackup()
|
||||||
{
|
{
|
||||||
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
|
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
|
||||||
|
|||||||
@@ -5,26 +5,34 @@ namespace CarCareTracker.Helper
|
|||||||
public interface IGasHelper
|
public interface IGasHelper
|
||||||
{
|
{
|
||||||
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
|
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 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 minMileage = results.Min(x => x.Mileage);
|
var recordWithCalculatedMPG = recordsToCalculateGallons.Where(x => x.MilesPerGallon > 0);
|
||||||
|
var mileageAdjustment = results.Where(x => x.MissedFuelUp).Sum(y => y.DeltaMileage);
|
||||||
if (recordWithCalculatedMPG.Any())
|
if (recordWithCalculatedMPG.Any())
|
||||||
{
|
{
|
||||||
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
|
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
|
||||||
var totalGallonsConsumed = recordWithCalculatedMPG.Sum(x => x.Gallons);
|
var minMileage = recordWithCalculatedMPG.Min(x => x.Mileage);
|
||||||
var deltaMileage = maxMileage - minMileage;
|
var totalGallonsConsumed = recordsToCalculateGallons.Sum(x => x.Gallons);
|
||||||
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed;
|
var deltaMileage = maxMileage - minMileage - mileageAdjustment;
|
||||||
|
var averageGasMileage = (deltaMileage) / totalGallonsConsumed;
|
||||||
|
if (!useMPG)
|
||||||
|
{
|
||||||
|
averageGasMileage = 100 / averageGasMileage;
|
||||||
|
}
|
||||||
return averageGasMileage.ToString("F");
|
return averageGasMileage.ToString("F");
|
||||||
}
|
}
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
|
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>();
|
var computedResults = new List<GasRecordViewModel>();
|
||||||
int previousMileage = 0;
|
int previousMileage = 0;
|
||||||
decimal unFactoredConsumption = 0.00M;
|
decimal unFactoredConsumption = 0.00M;
|
||||||
@@ -73,9 +81,16 @@ namespace CarCareTracker.Helper
|
|||||||
else if (currentObject.IsFillToFull)
|
else if (currentObject.IsFillToFull)
|
||||||
{
|
{
|
||||||
//if user filled to full.
|
//if user filled to full.
|
||||||
if (convertedConsumption > 0.00M)
|
if (convertedConsumption > 0.00M && deltaMileage > 0)
|
||||||
{
|
{
|
||||||
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
|
try
|
||||||
|
{
|
||||||
|
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
gasRecordViewModel.MilesPerGallon = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//reset unFactored vars
|
//reset unFactored vars
|
||||||
unFactoredConsumption = 0;
|
unFactoredConsumption = 0;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace CarCareTracker.Helper
|
|||||||
{
|
{
|
||||||
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
|
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
|
||||||
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
|
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
|
||||||
|
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
|
||||||
}
|
}
|
||||||
public class MailHelper : IMailHelper
|
public class MailHelper : IMailHelper
|
||||||
{
|
{
|
||||||
@@ -60,20 +61,62 @@ namespace CarCareTracker.Helper
|
|||||||
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
|
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 to = emailTo;
|
||||||
string from = mailConfig.EmailFrom;
|
string from = mailConfig.EmailFrom;
|
||||||
var server = mailConfig.EmailServer;
|
var server = mailConfig.EmailServer;
|
||||||
MailMessage message = new MailMessage(from, to);
|
MailMessage message = new MailMessage(from, to);
|
||||||
message.Subject = emailSubject;
|
message.Subject = emailSubject;
|
||||||
message.Body = emailBody;
|
message.Body = emailBody;
|
||||||
|
message.IsBodyHtml = isBodyHtml;
|
||||||
SmtpClient client = new SmtpClient(server);
|
SmtpClient client = new SmtpClient(server);
|
||||||
client.EnableSsl = mailConfig.UseSSL;
|
client.EnableSsl = mailConfig.UseSSL;
|
||||||
client.Port = mailConfig.Port;
|
client.Port = mailConfig.Port;
|
||||||
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
|
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
client.Send(message);
|
if (useAsync)
|
||||||
|
{
|
||||||
|
client.SendMailAsync(message, new CancellationToken());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
client.Send(message);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -14,11 +14,24 @@ namespace CarCareTracker.Helper
|
|||||||
if (existingReminder.Metric == ReminderMetric.Both)
|
if (existingReminder.Metric == ReminderMetric.Both)
|
||||||
{
|
{
|
||||||
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
|
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
|
||||||
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
|
||||||
|
{
|
||||||
|
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingReminder.Mileage += existingReminder.CustomMileageInterval;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (existingReminder.Metric == ReminderMetric.Odometer)
|
else if (existingReminder.Metric == ReminderMetric.Odometer)
|
||||||
{
|
{
|
||||||
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other)
|
||||||
|
{
|
||||||
|
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
existingReminder.Mileage += existingReminder.CustomMileageInterval;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (existingReminder.Metric == ReminderMetric.Date)
|
else if (existingReminder.Metric == ReminderMetric.Date)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,5 +100,71 @@ namespace CarCareTracker.Helper
|
|||||||
new CostForVehicleByMonth { MonthId = 12, 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,
|
||||||
|
Tags = input.Tags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Tags = input.Tags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Tags = input.Tags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFuelEconomyUnit(bool useKwh, bool useHours, bool useMPG, bool useUKMPG)
|
||||||
|
{
|
||||||
|
string fuelEconomyUnit;
|
||||||
|
if (useKwh)
|
||||||
|
{
|
||||||
|
var distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
|
||||||
|
fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
|
||||||
|
}
|
||||||
|
else if (useMPG && useUKMPG)
|
||||||
|
{
|
||||||
|
fuelEconomyUnit = useHours ? "h/g" : "mpg";
|
||||||
|
}
|
||||||
|
else if (useUKMPG)
|
||||||
|
{
|
||||||
|
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
|
||||||
|
}
|
||||||
|
return fuelEconomyUnit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
namespace CarCareTracker.Models
|
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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||||
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
|
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags }; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
{
|
{
|
||||||
public class GasRecordInputContainer
|
public class GasRecordInputContainer
|
||||||
{
|
{
|
||||||
public bool UseKwh { get; set; }
|
public bool UseKwh { get; set; }
|
||||||
public GasRecordInput GasRecord { get; set; }
|
public bool UseHours { get; set; }
|
||||||
|
public GasRecordInput GasRecord { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
{
|
{
|
||||||
public class GasRecordViewModelContainer
|
public class GasRecordViewModelContainer
|
||||||
{
|
{
|
||||||
public bool UseKwh { get; set; }
|
public bool UseKwh { get; set; }
|
||||||
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
|
public bool UseHours { get; set; }
|
||||||
|
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
Models/GenericRecord.cs
Normal file
15
Models/GenericRecord.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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>();
|
||||||
|
public List<string> Tags { get; set;} = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,6 @@
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string NoteText { get; set; }
|
public string NoteText { get; set; }
|
||||||
public bool Pinned { get; set; }
|
public bool Pinned { get; set; }
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
public int Mileage { get; set; }
|
public int Mileage { get; set; }
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
public int Mileage { get; set; }
|
public int Mileage { get; set; }
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files }; }
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
|
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files, Tags = Tags }; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public bool IsRecurring { get; set; } = false;
|
public bool IsRecurring { get; set; } = false;
|
||||||
|
public int CustomMileageInterval { get; set; } = 0;
|
||||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public bool IsRecurring { get; set; } = false;
|
public bool IsRecurring { get; set; } = false;
|
||||||
|
public int CustomMileageInterval { get; set; } = 0;
|
||||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
IsRecurring = IsRecurring,
|
IsRecurring = IsRecurring,
|
||||||
ReminderMileageInterval = ReminderMileageInterval,
|
ReminderMileageInterval = ReminderMileageInterval,
|
||||||
ReminderMonthInterval = ReminderMonthInterval,
|
ReminderMonthInterval = ReminderMonthInterval,
|
||||||
|
CustomMileageInterval = CustomMileageInterval,
|
||||||
Notes = Notes }; }
|
Notes = Notes }; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,6 @@
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public decimal Cost { get; set; }
|
public decimal Cost { get; set; }
|
||||||
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
namespace CarCareTracker.Models
|
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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||||
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
|
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags }; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,6 @@
|
|||||||
public bool IsRecurring { get; set; } = false;
|
public bool IsRecurring { get; set; } = false;
|
||||||
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
|
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
public bool IsRecurring { get; set; } = false;
|
public bool IsRecurring { get; set; } = false;
|
||||||
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
|
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
public TaxRecord ToTaxRecord() { return new TaxRecord {
|
public TaxRecord ToTaxRecord() { return new TaxRecord {
|
||||||
Id = Id,
|
Id = Id,
|
||||||
VehicleId = VehicleId,
|
VehicleId = VehicleId,
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
Notes = Notes,
|
Notes = Notes,
|
||||||
IsRecurring = IsRecurring,
|
IsRecurring = IsRecurring,
|
||||||
RecurringInterval = RecurringInterval,
|
RecurringInterval = RecurringInterval,
|
||||||
Files = Files }; }
|
Files = Files,
|
||||||
|
Tags = Tags
|
||||||
|
}; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
namespace CarCareTracker.Models
|
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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||||
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
|
||||||
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
|
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files, Tags = Tags }; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
public bool HideZero { get; set; }
|
public bool HideZero { get; set; }
|
||||||
public bool UseUKMPG {get;set;}
|
public bool UseUKMPG {get;set;}
|
||||||
public bool UseThreeDecimalGasCost { get; set; }
|
public bool UseThreeDecimalGasCost { get; set; }
|
||||||
|
public bool UseMarkDownOnSavedNotes { get; set; }
|
||||||
public bool EnableAutoReminderRefresh { get; set; }
|
public bool EnableAutoReminderRefresh { get; set; }
|
||||||
public bool EnableAutoOdometerInsert { get; set; }
|
public bool EnableAutoOdometerInsert { get; set; }
|
||||||
|
public string PreferredGasUnit { get; set; } = string.Empty;
|
||||||
|
public string PreferredGasMileageUnit { get; set; } = string.Empty;
|
||||||
public string UserNameHash { get; set; }
|
public string UserNameHash { get; set; }
|
||||||
public string UserPasswordHash { get; set;}
|
public string UserPasswordHash { get; set;}
|
||||||
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
|
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
|
||||||
|
|||||||
@@ -9,5 +9,7 @@
|
|||||||
public string Model { get; set; }
|
public string Model { get; set; }
|
||||||
public string LicensePlate { get; set; }
|
public string LicensePlate { get; set; }
|
||||||
public bool IsElectric { get; set; } = false;
|
public bool IsElectric { get; set; } = false;
|
||||||
|
public bool UseHours { get; set; } = false;
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
A self-hosted, open-source vehicle service records and maintainence tracker.
|
A self-hosted, open-source vehicle service records and maintainence tracker.
|
||||||
|
|
||||||
Support this project on Patreon: https://patreon.com/LubeLogger
|
Visit our website: https://lubelogger.com
|
||||||
|
|
||||||
|
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
|
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
|
||||||
@@ -10,6 +12,11 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
<a href="/docs/screenshots.md">Screenshots</a>
|
<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
|
## Dependencies
|
||||||
- Bootstrap
|
- Bootstrap
|
||||||
- LiteDB
|
- LiteDB
|
||||||
@@ -17,12 +24,13 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
|
|||||||
- SweetAlert2
|
- SweetAlert2
|
||||||
- CsvHelper
|
- CsvHelper
|
||||||
- Chart.js
|
- Chart.js
|
||||||
|
- Drawdown
|
||||||
|
|
||||||
## Docker Setup (GHCR)
|
## Docker Setup (GHCR)
|
||||||
1. Install Docker
|
1. Install Docker
|
||||||
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
|
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.
|
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`
|
5. Run `docker-compose up`
|
||||||
|
|
||||||
## Docker Setup (Manual Build)
|
## 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.
|
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 .`
|
4. Run `docker build -t lubelogger -f Dockerfile .`
|
||||||
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
|
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`
|
7. Run `docker-compose up`
|
||||||
|
|
||||||
## Additional Docker Instructions
|
## Additional Docker Instructions
|
||||||
|
|||||||
@@ -241,6 +241,21 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
@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="row">
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
GET
|
GET
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
@inject IConfigHelper config
|
@inject IConfigHelper config
|
||||||
@{
|
@{
|
||||||
var enableAuth = config.GetUserConfig(User).EnableAuth;
|
var enableAuth = config.GetUserConfig(User).EnableAuth;
|
||||||
|
var logoUrl = config.GetLogoUrl();
|
||||||
}
|
}
|
||||||
@model string
|
@model string
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "LubeLogger";
|
ViewData["Title"] = "LubeLogger";
|
||||||
}
|
}
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/garage.js" asp-append-version="true"></script>
|
<script src="~/js/garage.js"></script>
|
||||||
}
|
}
|
||||||
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
||||||
<ul class="navbar-nav" id="homeTab" role="tablist">
|
<ul class="navbar-nav" id="homeTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>Garage</span></button>
|
<button class="nav-link user-select-none @(Model == "garage" ? "active" : "")" ontouchstart="detectLongTouch(this)" ontouchend="detectTouchEndPremature(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>Garage</span></button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button>
|
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button>
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="d-flex lubelogger-navbar">
|
<div class="d-flex lubelogger-navbar">
|
||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="@logoUrl" />
|
||||||
<div class="lubelogger-navbar-button">
|
<div class="lubelogger-navbar-button">
|
||||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
|
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>Garage</button>
|
<button class="nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>Garage</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item ms-auto" role="presentation">
|
<li class="nav-item ms-auto" role="presentation">
|
||||||
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>Settings</button>
|
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>Settings</button>
|
||||||
@@ -69,9 +70,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="homeTab">
|
<div class="tab-content" id="homeTab">
|
||||||
<div class="tab-pane fade @(Model == "garage" ? "show active" : "")" id="garage-tab-pane" role="tabpanel" tabindex="0">
|
<div class="tab-pane fade @(Model == "garage" ? "show active" : "")" id="garage-tab-pane" role="tabpanel" tabindex="0">
|
||||||
<div class="row">
|
<div id="garageContainer">
|
||||||
<div id="garageContainer" class="row gy-3 align-items-stretch">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
|
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
@model List<Vehicle>
|
@model List<Vehicle>
|
||||||
|
@{
|
||||||
@if (Model.Any())
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
|
}
|
||||||
|
@if (recordTags.Any())
|
||||||
{
|
{
|
||||||
foreach (Vehicle vehicle in Model)
|
<div class='row'>
|
||||||
{
|
<div class="d-flex align-items-center flex-wrap mt-4">
|
||||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4">
|
@foreach (string recordTag in recordTags)
|
||||||
<div class="card" onclick="viewVehicle(@vehicle.Id)">
|
{
|
||||||
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
|
<span onclick="filterGarage(this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
<div class="card-body">
|
}
|
||||||
<h5 class="card-title text-truncate">@($"{vehicle.Year}")</h5>
|
<datalist id="tagList">
|
||||||
<h5 class="card-title text-truncate">@($"{vehicle.Make}")</h5>
|
@foreach (string recordTag in recordTags)
|
||||||
<h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5>
|
{
|
||||||
<p class="card-text text-truncate">@vehicle.LicensePlate</p>
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="row gy-3 align-items-stretch vehiclesContainer">
|
||||||
|
@foreach (Vehicle vehicle in Model)
|
||||||
|
{
|
||||||
|
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none garage-item" data-tags='@string.Join(" ", vehicle.Tags)' id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
|
||||||
|
<div class="card" onclick="viewVehicle(@vehicle.Id)">
|
||||||
|
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-truncate garage-item-year" data-unit="@vehicle.Year">@($"{vehicle.Year}")</h5>
|
||||||
|
<h5 class="card-title text-truncate">@($"{vehicle.Make}")</h5>
|
||||||
|
<h5 class="card-title text-truncate">@($"{vehicle.Model}")</h5>
|
||||||
|
<p class="card-text text-truncate">@vehicle.LicensePlate</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 garage-item-add">
|
||||||
|
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
|
||||||
|
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
}
|
|
||||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4">
|
|
||||||
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
|
|
||||||
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
|
<input id="preferredGasUnit" style="display:none;" value="@Model.PreferredGasUnit"/>
|
||||||
|
<input id="preferredFuelMileageUnit" style="display:none;" value="@Model.PreferredGasMileageUnit" />
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UseDarkMode">
|
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UseDarkMode">
|
||||||
<label class="form-check-label" for="enableDarkMode">Dark Mode</label>
|
<label class="form-check-label" for="enableDarkMode">Dark Mode</label>
|
||||||
@@ -35,6 +37,10 @@
|
|||||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UseThreeDecimalGasCost">
|
<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>
|
<label class="form-check-label" for="useThreeDecimal">Use Three Decimals For Fuel Cost</label>
|
||||||
</div>
|
</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">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoReminderRefresh" checked="@Model.EnableAutoReminderRefresh">
|
<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>
|
<label class="form-check-label" for="enableAutoReminderRefresh">Auto Refresh Lapsed Recurring Reminders</label>
|
||||||
@@ -154,14 +160,14 @@
|
|||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="/defaults/lubelogger_logo.png" />
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-center">
|
<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.1</small>
|
||||||
</div>
|
</div>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
|
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
|
||||||
</p>
|
</p>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
If you enjoyed using this app, please consider spreading the good word.<br />
|
If you enjoyed using this app, please consider spreading the good word.<br />
|
||||||
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a>
|
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a> or make a <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://buy.stripe.com/aEU9Egc8DdMc9bO144" target="_blank">donation</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
|
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
|
||||||
@@ -187,6 +193,7 @@
|
|||||||
<li class="list-group-item">SweetAlert2</li>
|
<li class="list-group-item">SweetAlert2</li>
|
||||||
<li class="list-group-item">CsvHelper</li>
|
<li class="list-group-item">CsvHelper</li>
|
||||||
<li class="list-group-item">Chart.js</li>
|
<li class="list-group-item">Chart.js</li>
|
||||||
|
<li class="list-group-item">Drawdown</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,8 +218,11 @@
|
|||||||
hideZero: $("#hideZero").is(":checked"),
|
hideZero: $("#hideZero").is(":checked"),
|
||||||
useUKMpg: $("#useUKMPG").is(":checked"),
|
useUKMpg: $("#useUKMPG").is(":checked"),
|
||||||
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
|
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
|
||||||
|
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
|
||||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||||
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
||||||
|
preferredGasUnit: $("#preferredGasUnit").val(),
|
||||||
|
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
|
||||||
visibleTabs: visibleTabs,
|
visibleTabs: visibleTabs,
|
||||||
defaultTab: defaultTab
|
defaultTab: defaultTab
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
@{
|
@using CarCareTracker.Helper
|
||||||
|
@inject IConfigHelper config
|
||||||
|
@{
|
||||||
|
var logoUrl = config.GetLogoUrl();
|
||||||
|
}
|
||||||
|
@{
|
||||||
ViewData["Title"] = "LubeLogger - Login";
|
ViewData["Title"] = "LubeLogger - Login";
|
||||||
}
|
}
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
<script src="~/js/login.js"></script>
|
||||||
}
|
}
|
||||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="@logoUrl" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputUserName">Username</label>
|
<label for="inputUserName">Username</label>
|
||||||
<input type="text" id="inputUserName" class="form-control">
|
<input type="text" id="inputUserName" class="form-control">
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
@{
|
@using CarCareTracker.Helper
|
||||||
|
@inject IConfigHelper config
|
||||||
|
@{
|
||||||
|
var logoUrl = config.GetLogoUrl();
|
||||||
|
}
|
||||||
|
@{
|
||||||
ViewData["Title"] = "LubeLogger - Login";
|
ViewData["Title"] = "LubeLogger - Login";
|
||||||
}
|
}
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
<script src="~/js/login.js"></script>
|
||||||
}
|
}
|
||||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="@logoUrl" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputUserName">Username</label>
|
<label for="inputUserName">Username</label>
|
||||||
<input type="text" id="inputUserName" class="form-control">
|
<input type="text" id="inputUserName" class="form-control">
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
@{
|
@using CarCareTracker.Helper
|
||||||
|
@inject IConfigHelper config
|
||||||
|
@{
|
||||||
|
var logoUrl = config.GetLogoUrl();
|
||||||
|
}
|
||||||
|
@{
|
||||||
ViewData["Title"] = "LubeLogger - Register";
|
ViewData["Title"] = "LubeLogger - Register";
|
||||||
}
|
}
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
<script src="~/js/login.js"></script>
|
||||||
}
|
}
|
||||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="@logoUrl" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputToken">Token</label>
|
<label for="inputToken">Token</label>
|
||||||
<input type="text" id="inputToken" class="form-control">
|
<input type="text" id="inputToken" class="form-control">
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
@{
|
@using CarCareTracker.Helper
|
||||||
|
@inject IConfigHelper config
|
||||||
|
@{
|
||||||
|
var logoUrl = config.GetLogoUrl();
|
||||||
|
}
|
||||||
|
@{
|
||||||
ViewData["Title"] = "LubeLogger - Register";
|
ViewData["Title"] = "LubeLogger - Register";
|
||||||
}
|
}
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
<script src="~/js/login.js"></script>
|
||||||
}
|
}
|
||||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<img src="/defaults/lubelogger_logo.png" />
|
<img src="@logoUrl" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputToken">Token</label>
|
<label for="inputToken">Token</label>
|
||||||
<input type="text" id="inputToken" class="form-control">
|
<input type="text" id="inputToken" class="form-control">
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
var userConfig = config.GetUserConfig(User);
|
var userConfig = config.GetUserConfig(User);
|
||||||
var useDarkMode = userConfig.UseDarkMode;
|
var useDarkMode = userConfig.UseDarkMode;
|
||||||
var enableCsvImports = userConfig.EnableCsvImports;
|
var enableCsvImports = userConfig.EnableCsvImports;
|
||||||
|
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
|
||||||
|
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
||||||
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||||
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
|
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
|
||||||
shortDatePattern = shortDatePattern.ToLower();
|
shortDatePattern = shortDatePattern.ToLower();
|
||||||
@@ -28,9 +30,10 @@
|
|||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
|
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
|
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/lib/bootstrap-tagsinput/bootstrap-tagsinput.css" />
|
||||||
<link rel="stylesheet" href="~/css/loader.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css"/>
|
||||||
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/loader.css"/>
|
||||||
|
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css"/>
|
||||||
<link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" />
|
<link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" />
|
||||||
<link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
<link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||||
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||||
@@ -41,13 +44,17 @@
|
|||||||
<script src="~/js/shared.js"></script>
|
<script src="~/js/shared.js"></script>
|
||||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="~/lib/bootstrap-datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="~/lib/bootstrap-datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
|
<script src="~/lib/bootstrap-tagsinput/bootstrap-tagsinput.js"></script>
|
||||||
<script src="~/sweetalert/sweetalert2.all.min.js"></script>
|
<script src="~/sweetalert/sweetalert2.all.min.js"></script>
|
||||||
<script src="~/js/loader.js"></script>
|
<script src="~/js/loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function getGlobalConfig() {
|
function getGlobalConfig() {
|
||||||
return {
|
return {
|
||||||
useDarkMode : "@useDarkMode" == "True",
|
useDarkMode : "@useDarkMode" == "True",
|
||||||
enableCsvImport : "@enableCsvImports" == "True"
|
enableCsvImport : "@enableCsvImports" == "True",
|
||||||
|
useMarkDown: "@useMarkDown" == "True",
|
||||||
|
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
|
||||||
|
useThreeDecimals: "@useThreeDecimals" == "True"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getShortDatePattern() {
|
function getShortDatePattern() {
|
||||||
@@ -59,10 +66,16 @@
|
|||||||
//remove thousands separator.
|
//remove thousands separator.
|
||||||
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
|
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
|
||||||
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
|
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
|
||||||
|
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
|
||||||
|
if (input == "---") {
|
||||||
|
input = "0";
|
||||||
|
}
|
||||||
//strip thousands from input.
|
//strip thousands from input.
|
||||||
input = input.replace(thousandSeparator, "");
|
input = input.replace(thousandSeparator, "");
|
||||||
//convert to JS format where decimal is only separated by .
|
//convert to JS format where decimal is only separated by .
|
||||||
input = input.replace(decimalSeparator, ".");
|
input = input.replace(decimalSeparator, ".");
|
||||||
|
//remove any currency symbol
|
||||||
|
input = input.replace(currencySymbol, "");
|
||||||
return parseFloat(input);
|
return parseFloat(input);
|
||||||
}
|
}
|
||||||
function globalFloatToString(input) {
|
function globalFloatToString(input) {
|
||||||
|
|||||||
@@ -8,19 +8,20 @@
|
|||||||
}
|
}
|
||||||
@model Vehicle
|
@model Vehicle
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/vehicle.js" asp-append-version="true"></script>
|
<script src="~/js/vehicle.js"></script>
|
||||||
<script src="~/js/servicerecord.js" asp-append-version="true"></script>
|
<script src="~/js/servicerecord.js"></script>
|
||||||
<script src="~/js/gasrecord.js" asp-append-version="true"></script>
|
<script src="~/js/gasrecord.js"></script>
|
||||||
<script src="~/js/collisionrecord.js" asp-append-version="true"></script>
|
<script src="~/js/collisionrecord.js"></script>
|
||||||
<script src="~/js/taxrecord.js" asp-append-version="true"></script>
|
<script src="~/js/taxrecord.js"></script>
|
||||||
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
|
<script src="~/js/reminderrecord.js"></script>
|
||||||
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
|
<script src="~/js/upgraderecord.js"></script>
|
||||||
<script src="~/js/note.js" asp-append-version="true"></script>
|
<script src="~/js/note.js"></script>
|
||||||
<script src="~/js/reports.js" asp-append-version="true"></script>
|
<script src="~/js/reports.js"></script>
|
||||||
<script src="~/js/supplyrecord.js" asp-append-version="true"></script>
|
<script src="~/js/supplyrecord.js"></script>
|
||||||
<script src="~/js/planrecord.js" asp-append-version="true"></script>
|
<script src="~/js/planrecord.js"></script>
|
||||||
<script src="~/js/odometerrecord.js" asp-append-version="true"></script>
|
<script src="~/js/odometerrecord.js"></script>
|
||||||
<script src="~/lib/chart-js/chart.umd.js"></script>
|
<script src="~/lib/chart-js/chart.umd.js"></script>
|
||||||
|
<script src="~/lib/drawdown/drawdown.js"></script>
|
||||||
}
|
}
|
||||||
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
||||||
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
|
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="reminderRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" data-bs-focus="false" id="reminderRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content" id="reminderRecordModalContent">
|
<div class="modal-content" id="reminderRecordModalContent">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,9 +27,16 @@
|
|||||||
{
|
{
|
||||||
@await Html.PartialAsync("_SupplyStore", "RepairRecord")
|
@await Html.PartialAsync("_SupplyStore", "RepairRecord")
|
||||||
}
|
}
|
||||||
|
<label for="collisionRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="collisionRecordTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -37,6 +44,7 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="collisionRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -52,6 +60,7 @@
|
|||||||
}
|
}
|
||||||
<label for="collisionRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +70,17 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@if (!isNew)
|
@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>
|
<button type="button" class="btn btn-secondary" onclick="hideAddCollisionRecordModal()">Cancel</button>
|
||||||
@if (isNew)
|
@if (isNew)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@{
|
@{
|
||||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
}
|
}
|
||||||
@model List<CollisionRecord>
|
@model List<CollisionRecord>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -10,6 +11,16 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Repair Records: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Repair Records: {Model.Count()}")</span>
|
||||||
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('accident-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (enableCsvImports)
|
@if (enableCsvImports)
|
||||||
@@ -22,6 +33,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('RepairRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -45,14 +58,14 @@
|
|||||||
<th scope="col" class="col-2 col-xl-1">Date</th>
|
<th scope="col" class="col-2 col-xl-1">Date</th>
|
||||||
<th scope="col" class="col-2">Odometer</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-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>
|
<th scope="col" class="col-3">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (CollisionRecord collisionRecord in Model)
|
@foreach (CollisionRecord collisionRecord in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
|
||||||
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
|
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
|
||||||
<td class="col-2">@collisionRecord.Mileage</td>
|
<td class="col-2">@collisionRecord.Mileage</td>
|
||||||
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
|
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
labels: ["Service Records", "Repairs", "Upgrades", "Tax", "Fuel"],
|
labels: ["Service Records", "Repairs", "Upgrades", "Tax", "Fuel"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Expenses by Category",
|
label: "Expenses by Type",
|
||||||
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
|
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
|
||||||
data: [
|
data: [
|
||||||
@Model.ServiceRecordSum,
|
globalParseFloat('@Model.ServiceRecordSum'),
|
||||||
@Model.CollisionRecordSum,
|
globalParseFloat('@Model.CollisionRecordSum'),
|
||||||
@Model.UpgradeRecordSum,
|
globalParseFloat('@Model.UpgradeRecordSum'),
|
||||||
@Model.TaxRecordSum,
|
globalParseFloat('@Model.TaxRecordSum'),
|
||||||
@Model.GasRecordSum
|
globalParseFloat('@Model.GasRecordSum')
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,28 +11,31 @@
|
|||||||
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
||||||
var gasCostFormat = useThreeDecimals ? "C3" : "C2";
|
var gasCostFormat = useThreeDecimals ? "C3" : "C2";
|
||||||
var useKwh = Model.UseKwh;
|
var useKwh = Model.UseKwh;
|
||||||
|
var useHours = Model.UseHours;
|
||||||
|
string preferredFuelEconomyUnit = userConfig.PreferredGasMileageUnit;
|
||||||
|
string preferredGasUnit = userConfig.PreferredGasUnit;
|
||||||
string consumptionUnit;
|
string consumptionUnit;
|
||||||
string fuelEconomyUnit;
|
string fuelEconomyUnit;
|
||||||
string distanceUnit = useMPG ? "mi." : "km";
|
string distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
|
||||||
if (useKwh)
|
if (useKwh)
|
||||||
{
|
{
|
||||||
consumptionUnit = "kWh";
|
consumptionUnit = "kWh";
|
||||||
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
|
fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}";
|
||||||
}
|
}
|
||||||
else if (useMPG && useUKMPG)
|
else if (useMPG && useUKMPG)
|
||||||
{
|
{
|
||||||
consumptionUnit = "imp gal";
|
consumptionUnit = "imp gal";
|
||||||
fuelEconomyUnit = "mpg";
|
fuelEconomyUnit = useHours ? "h/g" : "mpg";
|
||||||
} else if (useUKMPG)
|
} else if (useUKMPG)
|
||||||
{
|
{
|
||||||
fuelEconomyUnit = "l/100mi.";
|
fuelEconomyUnit = useHours ? "l/100h" : "l/100mi.";
|
||||||
consumptionUnit = "l";
|
consumptionUnit = "l";
|
||||||
distanceUnit = "mi.";
|
distanceUnit = useHours ? "h" : "mi.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
consumptionUnit = useMPG ? "US gal" : "l";
|
consumptionUnit = useMPG ? "US gal" : "l";
|
||||||
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
|
fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -41,9 +44,16 @@
|
|||||||
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
|
||||||
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
|
@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" id="averageFuelMileageLabel">@($"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>
|
if (useMPG)
|
||||||
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
{
|
||||||
|
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"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" id="maxFuelMileageLabel">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
<span class="ms-2 badge bg-primary" id="minFuelMileageLabel">@($"Min Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||||
|
<span class="ms-2 badge bg-primary" id="maxFuelMileageLabel">@($"Max Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
|
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
|
||||||
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
|
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
|
||||||
@@ -58,6 +68,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('GasRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('GasRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
@@ -77,10 +89,10 @@
|
|||||||
<tr class="d-flex">
|
<tr class="d-flex">
|
||||||
<th scope="col" class="col-2">Date Refueled</th>
|
<th scope="col" class="col-2">Date Refueled</th>
|
||||||
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
|
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
|
||||||
<th scope="col" class="col-2">Consumption(@(consumptionUnit))</th>
|
<th scope="col" class="col-2" data-gas="consumption" data-unit="@consumptionUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">Consumption(@(consumptionUnit))</th>
|
||||||
<th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</th>
|
<th scope="col" class="col-4" data-gas="fueleconomy" data-unit="@fuelEconomyUnit" onclick="toggleSort('gas-tab-pane', this)" oncontextmenu="toggleUnits(this)" style="cursor:pointer;">Fuel Economy(@(fuelEconomyUnit))</th>
|
||||||
<th scope="col" class="col-1">Cost</th>
|
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Cost</th>
|
||||||
<th scope="col" class="col-1">Unit Cost</th>
|
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Unit Cost</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -89,10 +101,10 @@
|
|||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
|
||||||
<td class="col-2">@gasRecord.Date</td>
|
<td class="col-2">@gasRecord.Date</td>
|
||||||
<td class="col-2">@gasRecord.Mileage</td>
|
<td class="col-2">@gasRecord.Mileage</td>
|
||||||
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
|
<td class="col-2" data-gas-type="consumption">@gasRecord.Gallons.ToString("F")</td>
|
||||||
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
<td class="col-4" data-gas-type="fueleconomy">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||||
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
|
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
|
||||||
<td class="col-1">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
|
<td class="col-1" data-gas-type="unitcost">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -107,4 +119,15 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(preferredFuelEconomyUnit))
|
||||||
|
{
|
||||||
|
@:convertFuelMileageUnits(decodeHTMLEntities('@fuelEconomyUnit'), decodeHTMLEntities('@preferredFuelEconomyUnit'), false);
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(preferredGasUnit))
|
||||||
|
{
|
||||||
|
@:convertGasConsumptionUnits(decodeHTMLEntities('@consumptionUnit'), decodeHTMLEntities('@preferredGasUnit'), false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||||
{
|
{
|
||||||
@:barGraphLabels.push("@gasCost.MonthName");
|
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||||
@:barGraphData.push(@gasCost.Cost);
|
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||||
@:barGraphColors.push('@barGraphColors[index]');
|
@:barGraphColors.push('@barGraphColors[index]');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
var useMPG = config.GetUserConfig(User).UseMPG;
|
var useMPG = config.GetUserConfig(User).UseMPG;
|
||||||
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
|
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
|
||||||
var useKwh = Model.UseKwh;
|
var useKwh = Model.UseKwh;
|
||||||
|
var useHours = Model.UseHours;
|
||||||
var isNew = Model.GasRecord.Id == 0;
|
var isNew = Model.GasRecord.Id == 0;
|
||||||
string consumptionUnit;
|
string consumptionUnit;
|
||||||
string distanceUnit;
|
string distanceUnit;
|
||||||
@@ -19,7 +20,11 @@
|
|||||||
{
|
{
|
||||||
consumptionUnit = useMPG ? "gallons" : "liters";
|
consumptionUnit = useMPG ? "gallons" : "liters";
|
||||||
}
|
}
|
||||||
if (useUKMPG)
|
if (useHours)
|
||||||
|
{
|
||||||
|
distanceUnit = "hours";
|
||||||
|
}
|
||||||
|
else if (useUKMPG)
|
||||||
{
|
{
|
||||||
distanceUnit = "miles";
|
distanceUnit = "miles";
|
||||||
}
|
}
|
||||||
@@ -73,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="gasRecordNotes" class="form-control" rows="5">@Model.GasRecord.Notes</textarea>
|
||||||
@if (Model.GasRecord.Files.Any())
|
@if (Model.GasRecord.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -81,12 +86,14 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
|
||||||
<label for="gasRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<label for="gasRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||||
{
|
{
|
||||||
@:barGraphLabels.push("@gasCost.MonthName");
|
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||||
@:barGraphData.push(@gasCost.Cost);
|
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||||
@:barGraphColors.push('@barGraphColors[index]');
|
@:barGraphColors.push('@barGraphColors[index]');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,18 @@
|
|||||||
<input type="text" id="noteDescription" class="form-control" placeholder="Description of the note" value="@(isNew ? "" : Model.Description)">
|
<input type="text" id="noteDescription" class="form-control" placeholder="Description of the note" value="@(isNew ? "" : Model.Description)">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<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>
|
<textarea class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="noteRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="noteRecordTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
@model List<Note>
|
@model List<Note>
|
||||||
|
@{
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Notes: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Notes: {Model.Count()}")</span>
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('notes-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="showAddNoteModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Note</button>
|
<button onclick="showAddNoteModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Note</button>
|
||||||
@@ -26,10 +39,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (Note note in Model)
|
@foreach (Note note in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)" data-tags='@string.Join(" ", note.Tags)'>
|
||||||
@if (note.Pinned)
|
@if (note.Pinned)
|
||||||
{
|
{
|
||||||
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description"</td>
|
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description</td>
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
<td class="col-3">@note.Description</td>
|
<td class="col-3">@note.Description</td>
|
||||||
|
|||||||
@@ -19,9 +19,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<label for="odometerRecordMileage">Odometer</label>
|
<label for="odometerRecordMileage">Odometer</label>
|
||||||
<input type="number" id="odometerRecordMileage" class="form-control" placeholder="Odometer reading" value="@(isNew ? "" : Model.Mileage)">
|
<input type="number" id="odometerRecordMileage" class="form-control" placeholder="Odometer reading" value="@(isNew ? "" : Model.Mileage)">
|
||||||
|
<label for="odometerRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="odometerRecordTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="odometerRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -29,12 +36,14 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="odometerRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<label for="odometerRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,23 @@
|
|||||||
@{
|
@{
|
||||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
}
|
}
|
||||||
@model List<OdometerRecord>
|
@model List<OdometerRecord>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Odometer Records: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Odometer Records: {Model.Count()}")</span>
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('odometer-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (enableCsvImports)
|
@if (enableCsvImports)
|
||||||
@@ -21,6 +32,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('OdometerRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('OdometerRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -49,7 +62,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (OdometerRecord odometerRecord in Model)
|
@foreach (OdometerRecord odometerRecord in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditOdometerRecordModal(@odometerRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditOdometerRecordModal(@odometerRecord.Id)" data-tags='@string.Join(" ", odometerRecord.Tags)'>
|
||||||
<td class="col-2 col-xl-1">@odometerRecord.Date.ToShortDateString()</td>
|
<td class="col-2 col-xl-1">@odometerRecord.Date.ToShortDateString()</td>
|
||||||
<td class="col-3">@odometerRecord.Mileage</td>
|
<td class="col-3">@odometerRecord.Mileage</td>
|
||||||
<td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
|
<td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="planRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -53,12 +53,14 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="planRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<label for="planRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,14 +39,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" onChange="enableRecurring()" role="switch" id="reminderIsRecurring" checked="@Model.IsRecurring">
|
<input class="form-check-input" type="checkbox" onChange="enableRecurring()" role="switch" id="reminderIsRecurring" checked="@Model.IsRecurring">
|
||||||
<label class="form-check-label" for="reminderIsRecurring">Is Recurring</label>
|
<label class="form-check-label" for="reminderIsRecurring">Is Recurring</label>
|
||||||
</div>
|
</div>
|
||||||
<label for="reminderRecurringMileage">Odometer</label>
|
<label for="reminderRecurringMileage">Odometer</label>
|
||||||
<select class="form-select" id="reminderRecurringMileage" @(Model.IsRecurring ? "" : "disabled")>
|
<select class="form-select" onchange="checkCustomMileageInterval()" id="reminderRecurringMileage" @(Model.IsRecurring ? "" : "disabled")>
|
||||||
|
<!option value="Other" @(Model.ReminderMileageInterval == ReminderMileageInterval.Other ? "selected" : "")>@(Model.ReminderMileageInterval == ReminderMileageInterval.Other && Model.CustomMileageInterval > 0 ? $"Other: {Model.CustomMileageInterval}" : "Other")</!option>
|
||||||
<!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option>
|
<!option value="FiftyMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiftyMiles ? "selected" : "")>50 mi. / Km</!option>
|
||||||
<!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option>
|
<!option value="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option>
|
||||||
<!option value="FiveHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiveHundredMiles ? "selected" : "")>500 mi. / Km</!option>
|
<!option value="FiveHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiveHundredMiles ? "selected" : "")>500 mi. / Km</!option>
|
||||||
@@ -59,7 +60,9 @@
|
|||||||
<!option value="FifteenThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FifteenThousandMiles ? "selected" : "")>15000 mi. / Km</!option>
|
<!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="TwentyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.TwentyThousandMiles ? "selected" : "")>20000 mi. / Km</!option>
|
||||||
<!option value="ThirtyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.ThirtyThousandMiles ? "selected" : "")>30000 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="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="OneHundredThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredThousandMiles ? "selected" : "")>100000 mi. / Km</!option>
|
||||||
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
|
<!option value="OneHundredFiftyThousandMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredFiftyThousandMiles ? "selected" : "")>150000 mi. / Km</!option>
|
||||||
</select>
|
</select>
|
||||||
@@ -93,7 +96,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
var customMileageInterval = @Model.CustomMileageInterval;
|
||||||
function getReminderRecordModelData() {
|
function getReminderRecordModelData() {
|
||||||
return { id: @Model.Id}
|
return { id: @Model.Id, mileageInterval: '@Model.ReminderMileageInterval.ToString()'}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,90 +1,93 @@
|
|||||||
@model ReportViewModel
|
@model ReportViewModel
|
||||||
<div class="container reportTabContainer">
|
<div class="container reportTabContainer">
|
||||||
<div class="row hideOnPrint">
|
<div class="row hideOnPrint">
|
||||||
<div class="col-md-3 col-12 mt-2">
|
<div class="col-md-3 col-12 mt-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<select class="form-select" id="yearOption" onchange="yearUpdated()">
|
<select class="form-select" id="yearOption" onchange="yearUpdated()">
|
||||||
<option value="0">All Time</option>
|
<option value="0">All Time</option>
|
||||||
@foreach (int year in Model.Years)
|
@foreach (int year in Model.Years)
|
||||||
{
|
{
|
||||||
<option value="@year">@year</option>
|
<option value="@year">@year</option>
|
||||||
}
|
}
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<div class="row">
|
<div class="col-md-6 col-12 mt-2">
|
||||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="costMakeUpReportContent">
|
<div class="row">
|
||||||
@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-1 d-sm-none d-md-block"></div>
|
<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>
|
||||||
<div class="row">
|
<div class="col-md-3 col-12 mt-2">
|
||||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="gasCostByMonthReportContent">
|
<div class="row">
|
||||||
@await Html.PartialAsync("_GasCostByMonthReport", Model.CostForVehicleByMonth)
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-12 mt-2">
|
<hr />
|
||||||
<div class="row">
|
<div class="row hideOnPrint">
|
||||||
<div class="col-12">
|
<div class="col-md-3 col-12 chartContainer" id="collaboratorContent">
|
||||||
<select class="form-select" onchange="updateReminderPie()" id="reminderOption">
|
@await Html.PartialAsync("_Collaborators", Model.Collaborators)
|
||||||
<option value="0">As of Today</option>
|
</div>
|
||||||
<option value="30">+30 Days</option>
|
<div class="col-md-6 col-12 chartContainer">
|
||||||
<option value="60">+60 Days</option>
|
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="monthFuelMileageReportContent">
|
||||||
<option value="90">+90 Days</option>
|
@await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth)
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="col-md-3 col-12 chartContainer">
|
||||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="reminderMakeUpReportContent">
|
<div class="d-grid">
|
||||||
@await Html.PartialAsync("_ReminderMakeUpReport", Model.ReminderMakeUpForVehicle)
|
<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>
|
||||||
</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>
|
<div id="vehicleHistoryReport" class="showOnPrint"></div>
|
||||||
@@ -27,9 +27,16 @@
|
|||||||
{
|
{
|
||||||
@await Html.PartialAsync("_SupplyStore", "ServiceRecord")
|
@await Html.PartialAsync("_SupplyStore", "ServiceRecord")
|
||||||
}
|
}
|
||||||
|
<label for="serviceRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="serviceRecordTag">
|
||||||
|
@foreach(string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -37,6 +44,7 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="serviceRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -52,6 +60,7 @@
|
|||||||
}
|
}
|
||||||
<label for="serviceRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +70,17 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@if (!isNew)
|
@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>
|
<button type="button" class="btn btn-secondary" onclick="hideAddServiceRecordModal()">Cancel</button>
|
||||||
@if (isNew)
|
@if (isNew)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@{
|
@{
|
||||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
}
|
}
|
||||||
@model List<ServiceRecord>
|
@model List<ServiceRecord>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -10,6 +11,16 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Service Records: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Service Records: {Model.Count()}")</span>
|
||||||
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||||
|
@foreach(string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('servicerecord-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (enableCsvImports)
|
@if (enableCsvImports)
|
||||||
@@ -22,6 +33,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('ServiceRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('ServiceRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -45,14 +58,14 @@
|
|||||||
<th scope="col" class="col-2 col-xl-1">Date</th>
|
<th scope="col" class="col-2 col-xl-1">Date</th>
|
||||||
<th scope="col" class="col-2">Odometer</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-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>
|
<th scope="col" class="col-3">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (ServiceRecord serviceRecord in Model)
|
@foreach (ServiceRecord serviceRecord in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditServiceRecordModal(@serviceRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditServiceRecordModal(@serviceRecord.Id)" data-tags='@string.Join(" ",serviceRecord.Tags)'>
|
||||||
<td class="col-2 col-xl-1">@serviceRecord.Date.ToShortDateString()</td>
|
<td class="col-2 col-xl-1">@serviceRecord.Date.ToShortDateString()</td>
|
||||||
<td class="col-2">@serviceRecord.Mileage</td>
|
<td class="col-2">@serviceRecord.Mileage</td>
|
||||||
<td class="col-3 col-xl-4">@serviceRecord.Description</td>
|
<td class="col-3 col-xl-4">@serviceRecord.Description</td>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="supplyRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -43,12 +43,14 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="supplyRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<label for="supplyRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('SupplyRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('SupplyRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -46,8 +48,8 @@
|
|||||||
<th scope="col" class="col-2">Part #</th>
|
<th scope="col" class="col-2">Part #</th>
|
||||||
<th scope="col" class="col-2">Supplier</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-2 col-xl-3">Description</th>
|
||||||
<th scope="col" class="col-1">Quantity</th>
|
<th scope="col" class="col-1" onclick="toggleSort('supply-tab-pane', this)" style="cursor:pointer;">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;">Cost</th>
|
||||||
<th scope="col" class="col-2">Notes</th>
|
<th scope="col" class="col-2">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -21,9 +21,16 @@
|
|||||||
<input type="text" id="taxRecordDescription" class="form-control" placeholder="Description of tax paid(i.e. Registration)" value="@Model.Description">
|
<input type="text" id="taxRecordDescription" class="form-control" placeholder="Description of tax paid(i.e. Registration)" value="@Model.Description">
|
||||||
<label for="taxRecordCost">Cost</label>
|
<label for="taxRecordCost">Cost</label>
|
||||||
<input type="text" id="taxRecordCost" class="form-control" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)">
|
<input type="text" id="taxRecordCost" class="form-control" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)">
|
||||||
|
<label for="taxRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="taxRecordTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring">
|
<input class="form-check-input" type="checkbox" onChange="enableTaxRecurring()" role="switch" id="taxIsRecurring" checked="@Model.IsRecurring">
|
||||||
@@ -45,6 +52,7 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="taxRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -60,6 +68,7 @@
|
|||||||
}
|
}
|
||||||
<label for="taxRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@{
|
@{
|
||||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
}
|
}
|
||||||
@model List<TaxRecord>
|
@model List<TaxRecord>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -10,6 +11,16 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Tax Records: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Tax Records: {Model.Count()}")</span>
|
||||||
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('tax-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (enableCsvImports)
|
@if (enableCsvImports)
|
||||||
@@ -22,6 +33,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('TaxRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('TaxRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -44,14 +57,14 @@
|
|||||||
<tr class="d-flex">
|
<tr class="d-flex">
|
||||||
<th scope="col" class="col-3 col-xl-1">Date</th>
|
<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-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>
|
<th scope="col" class="col-3">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (TaxRecord taxRecord in Model)
|
@foreach (TaxRecord taxRecord in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)" data-tags='@string.Join(" ", taxRecord.Tags)'>
|
||||||
<td class="col-3 col-xl-1">@taxRecord.Date.ToShortDateString()</td>
|
<td class="col-3 col-xl-1">@taxRecord.Date.ToShortDateString()</td>
|
||||||
<td class="col-4 col-xl-6">@taxRecord.Description</td>
|
<td class="col-4 col-xl-6">@taxRecord.Description</td>
|
||||||
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
|
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
|
||||||
|
|||||||
@@ -27,9 +27,16 @@
|
|||||||
{
|
{
|
||||||
@await Html.PartialAsync("_SupplyStore", "UpgradeRecord")
|
@await Html.PartialAsync("_SupplyStore", "UpgradeRecord")
|
||||||
}
|
}
|
||||||
|
<label for="upgradeRecordTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="upgradeRecordTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<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>
|
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||||
@if (Model.Files.Any())
|
@if (Model.Files.Any())
|
||||||
{
|
{
|
||||||
@@ -37,6 +44,7 @@
|
|||||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||||
<label for="upgradeRecordFiles">Upload more documents</label>
|
<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">
|
<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>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -52,6 +60,7 @@
|
|||||||
}
|
}
|
||||||
<label for="upgradeRecordFiles">Upload documents(optional)</label>
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +70,17 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@if (!isNew)
|
@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>
|
<button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">Cancel</button>
|
||||||
@if (isNew)
|
@if (isNew)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@{
|
@{
|
||||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
|
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||||
}
|
}
|
||||||
@model List<UpgradeRecord>
|
@model List<UpgradeRecord>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -10,6 +11,16 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<span class="ms-2 badge bg-success">@($"# of Upgrade Records: {Model.Count()}")</span>
|
<span class="ms-2 badge bg-success">@($"# of Upgrade Records: {Model.Count()}")</span>
|
||||||
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<span onclick="filterTable('upgrade-tab-pane', this)" class="user-select-none ms-2 rounded-pill badge bg-secondary tagfilter" style="cursor:pointer;">@recordTag</span>
|
||||||
|
}
|
||||||
|
<datalist id="tagList">
|
||||||
|
@foreach (string recordTag in recordTags)
|
||||||
|
{
|
||||||
|
<!option value="@recordTag"></!option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (enableCsvImports)
|
@if (enableCsvImports)
|
||||||
@@ -22,6 +33,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<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="showBulkImportModal('UpgradeRecord')">Import via CSV</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('UpgradeRecord')">Export to 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -45,14 +58,14 @@
|
|||||||
<th scope="col" class="col-2 col-xl-1">Date</th>
|
<th scope="col" class="col-2 col-xl-1">Date</th>
|
||||||
<th scope="col" class="col-2">Odometer</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-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>
|
<th scope="col" class="col-3">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (UpgradeRecord upgradeRecord in Model)
|
@foreach (UpgradeRecord upgradeRecord in Model)
|
||||||
{
|
{
|
||||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)">
|
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
|
||||||
<td class="col-2 col-xl-1">@upgradeRecord.Date.ToShortDateString()</td>
|
<td class="col-2 col-xl-1">@upgradeRecord.Date.ToShortDateString()</td>
|
||||||
<td class="col-2">@upgradeRecord.Mileage</td>
|
<td class="col-2">@upgradeRecord.Mileage</td>
|
||||||
<td class="col-3 col-xl-4">@upgradeRecord.Description</td>
|
<td class="col-3 col-xl-4">@upgradeRecord.Description</td>
|
||||||
|
|||||||
@@ -2,26 +2,6 @@
|
|||||||
@inject IConfigHelper config
|
@inject IConfigHelper config
|
||||||
@{
|
@{
|
||||||
var hideZero = config.GetUserConfig(User).HideZero;
|
var hideZero = config.GetUserConfig(User).HideZero;
|
||||||
var useMPG = config.GetUserConfig(User).UseMPG;
|
|
||||||
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
|
|
||||||
var useKwh = Model.VehicleData.IsElectric;
|
|
||||||
string fuelEconomyUnit;
|
|
||||||
if (useKwh)
|
|
||||||
{
|
|
||||||
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
|
|
||||||
}
|
|
||||||
else if (useMPG && useUKMPG)
|
|
||||||
{
|
|
||||||
fuelEconomyUnit = "mpg";
|
|
||||||
}
|
|
||||||
else if (useUKMPG)
|
|
||||||
{
|
|
||||||
fuelEconomyUnit = "l/100mi.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@model VehicleHistoryViewModel
|
@model VehicleHistoryViewModel
|
||||||
<div class="vehicleDetailTabContainer">
|
<div class="vehicleDetailTabContainer">
|
||||||
@@ -56,7 +36,7 @@
|
|||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item">Last Reported Odometer Reading: @Model.Odometer</li>
|
<li class="list-group-item">Last Reported Odometer Reading: @Model.Odometer</li>
|
||||||
<li class="list-group-item">Average Fuel Economy: @($"{Model.MPG} {fuelEconomyUnit}")</li>
|
<li class="list-group-item">Average Fuel Economy: @($"{Model.MPG}")</li>
|
||||||
<li class="list-group-item">Total Spent(excl. fuel): @Model.TotalCost.ToString("C")</li>
|
<li class="list-group-item">Total Spent(excl. fuel): @Model.TotalCost.ToString("C")</li>
|
||||||
<li class="list-group-item">Total Spent on Fuel: @Model.TotalGasCost.ToString("C")</li>
|
<li class="list-group-item">Total Spent on Fuel: @Model.TotalGasCost.ToString("C")</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
|
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
|
||||||
<label class="form-check-label" for="inputIsElectric">Electric Vehicle</label>
|
<label class="form-check-label" for="inputIsElectric">Electric Vehicle</label>
|
||||||
</div>
|
</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>
|
||||||
|
<label for="inputTag">Tags(optional)</label>
|
||||||
|
<select multiple class="form-select" id="inputTag">
|
||||||
|
@foreach (string tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<!option value="@tag">@tag</!option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
|
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
|
||||||
{
|
{
|
||||||
<label for="inputImage">Replace picture(optional)</label>
|
<label for="inputImage">Replace picture(optional)</label>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"EnableAutoOdometerInsert": false,
|
"EnableAutoOdometerInsert": false,
|
||||||
"UseUKMPG": false,
|
"UseUKMPG": false,
|
||||||
"UseThreeDecimalGasCost": true,
|
"UseThreeDecimalGasCost": true,
|
||||||
|
"UseMarkDownOnSavedNotes": false,
|
||||||
|
"PreferredGasMileageUnit": "",
|
||||||
|
"PreferredGasUnit": "",
|
||||||
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
|
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
|
||||||
"DefaultTab": 8,
|
"DefaultTab": 8,
|
||||||
"UserNameHash": "",
|
"UserNameHash": "",
|
||||||
|
|||||||
BIN
docs/dashboard.png
Normal file
BIN
docs/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
21
docs/documentation/Collaboration.md
Normal file
21
docs/documentation/Collaboration.md
Normal 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click on the little blue button with a User Add icon and you will be prompted to enter the user's username
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
Now the user can view and edit the vehicle as well:
|
||||||
|
|
||||||
|

|
||||||
51
docs/documentation/Configuring.md
Normal file
51
docs/documentation/Configuring.md
Normal 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.
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
Simply enter the credentials you have just set up and you will be logged right in
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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"
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
With SMTP Configured:
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
If you have SMTP Configured, the user will then receive an Email that looks similar to this:
|
||||||
|

|
||||||
|
|
||||||
|
The user can then proceed to Register for an account at your instance of LubeLogger.
|
||||||
|
|
||||||
75
docs/documentation/GettingStarted.md
Normal file
75
docs/documentation/GettingStarted.md
Normal 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
|
||||||
|

|
||||||
|

|
||||||
22
docs/documentation/Registration.md
Normal file
22
docs/documentation/Registration.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
They can do so by navigating to your LubeLogger instance and clicking on the "Register" link.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Then they just have to provide the token and their email address along with their set of credentials
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Note:** The email address and token are CASE SENSITIVE and MUST be identical to the email address that the token is generated for.
|
||||||
|
|
||||||
|

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

|
||||||
@@ -25,8 +25,16 @@
|
|||||||
<h6 class="display-6 text-center">Self-Hosted, Open-Source, Unconventionally-Named Vehicle Maintenance Records and Fuel Mileage Tracker</h6>
|
<h6 class="display-6 text-center">Self-Hosted, Open-Source, Unconventionally-Named Vehicle Maintenance Records and Fuel Mileage Tracker</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
|
||||||
<div class="row">
|
<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">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
<h6 class="display-6 text-center">Showcase</h6>
|
<h6 class="display-6 text-center">Showcase</h6>
|
||||||
</div>
|
</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="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="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="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>
|
||||||
<div class="carousel-inner">
|
<div class="carousel-inner">
|
||||||
<div class="carousel-item active">
|
<div class="carousel-item active">
|
||||||
@@ -47,6 +57,20 @@
|
|||||||
<p>All of your vehicles conveniently displayed in one place</p>
|
<p>All of your vehicles conveniently displayed in one place</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="carousel-item">
|
||||||
<img src="servicerecord.png" class="d-block w-100" alt="...">
|
<img src="servicerecord.png" class="d-block w-100" alt="...">
|
||||||
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
||||||
@@ -80,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row">
|
<div class="row" id="features">
|
||||||
<div class="col-12 d-flex justify-content-center">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
<h6 class="display-6 text-center">Features</h6>
|
<h6 class="display-6 text-center">Features</h6>
|
||||||
</div>
|
</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 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 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 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">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>
|
<li class="list-group-item">Attach documents for each record(receipts, invoices, etc)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<ul class="list-group">
|
<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">Dark Mode</li>
|
||||||
<li class="list-group-item">Mobile/Small screen support</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">Basic Authentication for security</li>
|
||||||
<li class="list-group-item">Coming Soon(API Endpoints)</li>
|
<li class="list-group-item">API Endpoints</li>
|
||||||
<li class="list-group-item">Coming Soon(Consolidated Report Export - Just like CarFax)</li>
|
<li class="list-group-item">Consolidated Vehicle Maintenance Report</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<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">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
<h6 class="display-6 text-center">Where to Download</h6>
|
<h6 class="display-6 text-center">Where to Download</h6>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +193,14 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 d-flex justify-content-center">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 d-flex justify-content-center">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
|
|||||||
BIN
docs/planner.png
Normal file
BIN
docs/planner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -300,4 +300,7 @@ input[type="file"] {
|
|||||||
}
|
}
|
||||||
[data-bs-theme=light] .taskCard {
|
[data-bs-theme=light] .taskCard {
|
||||||
background-color: rgba(80,80,80,0.25);
|
background-color: rgba(80,80,80,0.25);
|
||||||
|
}
|
||||||
|
.override-hide{
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
BIN
wwwroot/defaults/demo_default.zip
Normal file
BIN
wwwroot/defaults/demo_default.zip
Normal file
Binary file not shown.
@@ -4,6 +4,7 @@
|
|||||||
$("#collisionRecordModalContent").html(data);
|
$("#collisionRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#collisionRecordDate'));
|
initDatePicker($('#collisionRecordDate'));
|
||||||
|
initTagSelector($("#collisionRecordTag"));
|
||||||
$('#collisionRecordModal').modal('show');
|
$('#collisionRecordModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -14,7 +15,13 @@ function showEditCollisionRecordModal(collisionRecordId) {
|
|||||||
$("#collisionRecordModalContent").html(data);
|
$("#collisionRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#collisionRecordDate'));
|
initDatePicker($('#collisionRecordDate'));
|
||||||
|
initTagSelector($("#collisionRecordTag"));
|
||||||
$('#collisionRecordModal').modal('show');
|
$('#collisionRecordModal').modal('show');
|
||||||
|
$('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("collisionRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,10 +78,11 @@ function saveCollisionRecordToVehicle(isEdit) {
|
|||||||
}
|
}
|
||||||
function getAndValidateCollisionRecordValues() {
|
function getAndValidateCollisionRecordValues() {
|
||||||
var collisionDate = $("#collisionRecordDate").val();
|
var collisionDate = $("#collisionRecordDate").val();
|
||||||
var collisionMileage = $("#collisionRecordMileage").val();
|
var collisionMileage = parseInt(globalParseFloat($("#collisionRecordMileage").val())).toString();
|
||||||
var collisionDescription = $("#collisionRecordDescription").val();
|
var collisionDescription = $("#collisionRecordDescription").val();
|
||||||
var collisionCost = $("#collisionRecordCost").val();
|
var collisionCost = $("#collisionRecordCost").val();
|
||||||
var collisionNotes = $("#collisionRecordNotes").val();
|
var collisionNotes = $("#collisionRecordNotes").val();
|
||||||
|
var collisionTags = $("#collisionRecordTag").val();
|
||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var collisionRecordId = getCollisionRecordModelData().id;
|
var collisionRecordId = getCollisionRecordModelData().id;
|
||||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||||
@@ -115,6 +123,7 @@ function getAndValidateCollisionRecordValues() {
|
|||||||
notes: collisionNotes,
|
notes: collisionNotes,
|
||||||
files: uploadedFiles,
|
files: uploadedFiles,
|
||||||
supplies: selectedSupplies,
|
supplies: selectedSupplies,
|
||||||
|
tags: collisionTags,
|
||||||
addReminderRecord: addReminderRecord
|
addReminderRecord: addReminderRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
$.get('/Vehicle/AddVehiclePartialView', function (data) {
|
$.get('/Vehicle/AddVehiclePartialView', function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$("#addVehicleModalContent").html(data);
|
$("#addVehicleModalContent").html(data);
|
||||||
|
initTagSelector($("#inputTag"));
|
||||||
|
$('#addVehicleModal').modal('show');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
$('#addVehicleModal').modal('show');
|
|
||||||
}
|
}
|
||||||
function hideAddVehicleModal() {
|
function hideAddVehicleModal() {
|
||||||
$('#addVehicleModal').modal('hide');
|
$('#addVehicleModal').modal('hide');
|
||||||
@@ -28,4 +29,142 @@ function performLogOut() {
|
|||||||
window.location.href = '/Login';
|
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 {
|
||||||
|
//disable the tooltip
|
||||||
|
hoveredGrid.attr("data-bs-title", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (hoveredGrid.attr("data-bs-title") != '') {
|
||||||
|
hoveredGrid.tooltip("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hidePinnedNotes(vehicleId) {
|
||||||
|
if ($(`#gridVehicle_${vehicleId}`).attr('data-bs-title') != '') {
|
||||||
|
$(`#gridVehicle_${vehicleId}`).tooltip("hide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterGarage(sender, isSort) {
|
||||||
|
var rowData = $(".garage-item");
|
||||||
|
if (sender == undefined) {
|
||||||
|
rowData.removeClass('override-hide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var tagName = sender.textContent;
|
||||||
|
if ($(sender).hasClass("bg-primary")) {
|
||||||
|
if (!isSort) {
|
||||||
|
rowData.removeClass('override-hide');
|
||||||
|
$(sender).removeClass('bg-primary');
|
||||||
|
$(sender).addClass('bg-secondary');
|
||||||
|
} else {
|
||||||
|
rowData.addClass('override-hide');
|
||||||
|
$(`[data-tags~='${tagName}']`).removeClass('override-hide');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//hide table rows.
|
||||||
|
rowData.addClass('override-hide');
|
||||||
|
$(`[data-tags~='${tagName}']`).removeClass('override-hide');
|
||||||
|
if ($(".tagfilter.bg-primary").length > 0) {
|
||||||
|
//disabling other filters
|
||||||
|
$(".tagfilter.bg-primary").addClass('bg-secondary');
|
||||||
|
$(".tagfilter.bg-primary").removeClass('bg-primary');
|
||||||
|
}
|
||||||
|
$(sender).addClass('bg-primary');
|
||||||
|
$(sender).removeClass('bg-secondary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sortVehicles(desc) {
|
||||||
|
//get row data
|
||||||
|
var rowData = $('.garage-item');
|
||||||
|
var sortedRow = rowData.toArray().sort((a, b) => {
|
||||||
|
var currentVal = globalParseFloat($(a).find(".garage-item-year").attr('data-unit'));
|
||||||
|
var nextVal = globalParseFloat($(b).find(".garage-item-year").attr('data-unit'));
|
||||||
|
if (desc) {
|
||||||
|
return nextVal - currentVal;
|
||||||
|
} else {
|
||||||
|
return currentVal - nextVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sortedRow.push($('.garage-item-add'))
|
||||||
|
$('.vehiclesContainer').html(sortedRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
var touchtimer;
|
||||||
|
var touchduration = 800;
|
||||||
|
function detectLongTouch(sender) {
|
||||||
|
if ($(sender).hasClass("active")) {
|
||||||
|
if (!touchtimer) {
|
||||||
|
touchtimer = setTimeout(function () { sortGarage(sender, true); detectTouchEndPremature(sender); }, touchduration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function detectTouchEndPremature(sender) {
|
||||||
|
if (touchtimer) {
|
||||||
|
clearTimeout(touchtimer);
|
||||||
|
touchtimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortGarage(sender, isMobile) {
|
||||||
|
if (event != undefined) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
sender = $(sender);
|
||||||
|
if (sender.hasClass("active")) {
|
||||||
|
//do sorting only if garage is the active tab.
|
||||||
|
var sortColumn = sender.text();
|
||||||
|
var garageIcon = '<i class="bi bi-car-front me-2"></i>';
|
||||||
|
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>';
|
||||||
|
if (sender.hasClass('sort-asc')) {
|
||||||
|
sender.removeClass('sort-asc');
|
||||||
|
sender.addClass('sort-desc');
|
||||||
|
sender.html(isMobile ? `<span class="ms-2 display-3">${garageIcon}${sortColumn}${sortDescIcon}</span>` : `${garageIcon}${sortColumn}${sortDescIcon}`);
|
||||||
|
sortVehicles(true);
|
||||||
|
} else if (sender.hasClass('sort-desc')) {
|
||||||
|
//restore table
|
||||||
|
sender.removeClass('sort-desc');
|
||||||
|
sender.html(isMobile ? `<span class="ms-2 display-3">${garageIcon}${sortColumn}</span>` : `${garageIcon}${sortColumn}`);
|
||||||
|
$('.vehiclesContainer').html(storedTableRowState);
|
||||||
|
filterGarage($(".tagfilter.bg-primary").get(0), true);
|
||||||
|
} 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.
|
||||||
|
$('.vehiclesContainer').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(isMobile ? `<span class="ms-2 display-3">${garageIcon}${sortColumn}${sortAscIcon}</span>` : `${garageIcon}${sortColumn}${sortAscIcon}`);
|
||||||
|
storedTableRowState = null;
|
||||||
|
storedTableRowState = $('.vehiclesContainer').html();
|
||||||
|
sortVehicles(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
function showAddGasRecordModal() {
|
function showAddGasRecordModal() {
|
||||||
$.get('/Vehicle/GetAddGasRecordPartialView', function (data) {
|
$.get(`/Vehicle/GetAddGasRecordPartialView?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$("#gasRecordModalContent").html(data);
|
$("#gasRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
@@ -15,6 +15,11 @@ function showEditGasRecordModal(gasRecordId) {
|
|||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#gasRecordDate'));
|
initDatePicker($('#gasRecordDate'));
|
||||||
$('#gasRecordModal').modal('show');
|
$('#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() {
|
function getAndValidateGasRecordValues() {
|
||||||
var gasDate = $("#gasRecordDate").val();
|
var gasDate = $("#gasRecordDate").val();
|
||||||
var gasMileage = $("#gasRecordMileage").val();
|
var gasMileage = parseInt(globalParseFloat($("#gasRecordMileage").val())).toString();
|
||||||
var gasGallons = $("#gasRecordGallons").val();
|
var gasGallons = $("#gasRecordGallons").val();
|
||||||
var gasCost = $("#gasRecordCost").val();
|
var gasCost = $("#gasRecordCost").val();
|
||||||
var gasCostType = $("#gasCostType").val();
|
var gasCostType = $("#gasCostType").val();
|
||||||
@@ -127,4 +132,206 @@ function getAndValidateGasRecordValues() {
|
|||||||
missedFuelUp: gasIsMissed,
|
missedFuelUp: gasIsMissed,
|
||||||
notes: gasNotes
|
notes: gasNotes
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserGasTabPreferences() {
|
||||||
|
var gasUnit = $("[data-gas='consumption']").attr("data-unit");
|
||||||
|
var fuelMileageUnit = $("[data-gas='fueleconomy']").attr("data-unit");
|
||||||
|
$.post('/Vehicle/SaveUserGasTabPreferences', { gasUnit: gasUnit, fuelMileageUnit: fuelMileageUnit }, function (data) {
|
||||||
|
if (!data) {
|
||||||
|
errorToast("Error Saving User Preferences");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertGasConsumptionUnits(currentUnit, destinationUnit, save) {
|
||||||
|
var sender = $("[data-gas='consumption']");
|
||||||
|
if (currentUnit == "US gal") {
|
||||||
|
switch (destinationUnit) {
|
||||||
|
case "l":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
|
||||||
|
sender.attr("data-unit", "l");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
case "imp gal":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
|
||||||
|
sender.attr("data-unit", "imp gal");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentUnit == "l") {
|
||||||
|
switch (destinationUnit) {
|
||||||
|
case "US gal":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 3.785;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
|
||||||
|
sender.attr("data-unit", "US gal");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 3.785;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
case "imp gal":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal"));
|
||||||
|
sender.attr("data-unit", "imp gal");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentUnit == "imp gal") {
|
||||||
|
switch (destinationUnit) {
|
||||||
|
case "US gal":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 1.201;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "US gal"));
|
||||||
|
sender.attr("data-unit", "US gal");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 1.201;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
case "l":
|
||||||
|
$("[data-gas-type='consumption']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) * 4.546;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "l"));
|
||||||
|
sender.attr("data-unit", "l");
|
||||||
|
});
|
||||||
|
$("[data-gas-type='unitcost']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText) / 4.546;
|
||||||
|
var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2;
|
||||||
|
elem.innerText = `${getGlobalConfig().currencySymbol}${convertedAmount.toFixed(decimalPoints)}`;
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFuelMileageUnits(currentUnit, destinationUnit, save) {
|
||||||
|
var sender = $("[data-gas='fueleconomy']");
|
||||||
|
if (currentUnit == "l/100km") {
|
||||||
|
switch (destinationUnit) {
|
||||||
|
case "km/l":
|
||||||
|
$("[data-gas-type='fueleconomy']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText);
|
||||||
|
if (convertedAmount > 0) {
|
||||||
|
convertedAmount = 100 / convertedAmount;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
}
|
||||||
|
//update labels up top.
|
||||||
|
var newAverage = globalParseFloat($("#averageFuelMileageLabel").text().replace("Average Fuel Economy: ", ""));
|
||||||
|
if (newAverage > 0) {
|
||||||
|
newAverage = 100 / newAverage;
|
||||||
|
$("#averageFuelMileageLabel").text(`Average Fuel Economy: ${newAverage.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
var newMin = globalParseFloat($("#minFuelMileageLabel").text().replace("Min Fuel Economy: ", ""));
|
||||||
|
if (newMin > 0) {
|
||||||
|
newMin = 100 / newMin;
|
||||||
|
$("#minFuelMileageLabel").text(`Min Fuel Economy: ${newMin.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().replace("Max Fuel Economy: ", ""));
|
||||||
|
if (newMax > 0) {
|
||||||
|
newMax = 100 / newMax;
|
||||||
|
$("#maxFuelMileageLabel").text(`Max Fuel Economy: ${newMax.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "km/l"));
|
||||||
|
sender.attr("data-unit", "km/l");
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentUnit == "km/l") {
|
||||||
|
switch (destinationUnit) {
|
||||||
|
case "l/100km":
|
||||||
|
$("[data-gas-type='fueleconomy']").map((index, elem) => {
|
||||||
|
var convertedAmount = globalParseFloat(elem.innerText);
|
||||||
|
if (convertedAmount > 0) {
|
||||||
|
convertedAmount = 100 / convertedAmount;
|
||||||
|
elem.innerText = convertedAmount.toFixed(2);
|
||||||
|
}
|
||||||
|
var newAverage = globalParseFloat($("#averageFuelMileageLabel").text().replace("Average Fuel Economy: ", ""));
|
||||||
|
if (newAverage > 0) {
|
||||||
|
newAverage = 100 / newAverage;
|
||||||
|
$("#averageFuelMileageLabel").text(`Average Fuel Economy: ${newAverage.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
var newMin = globalParseFloat($("#minFuelMileageLabel").text().replace("Min Fuel Economy: ", ""));
|
||||||
|
if (newMin > 0) {
|
||||||
|
newMin = 100 / newMin;
|
||||||
|
$("#minFuelMileageLabel").text(`Min Fuel Economy: ${newMin.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
var newMax = globalParseFloat($("#maxFuelMileageLabel").text().replace("Max Fuel Economy: ", ""));
|
||||||
|
if (newMax > 0) {
|
||||||
|
newMax = 100 / newMax;
|
||||||
|
$("#maxFuelMileageLabel").text(`Max Fuel Economy: ${newMax.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
sender.text(sender.text().replace(sender.attr("data-unit"), "l/100km"));
|
||||||
|
sender.attr("data-unit", "l/100km");
|
||||||
|
});
|
||||||
|
if (save) { setDebounce(saveUserGasTabPreferences); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUnits(sender) {
|
||||||
|
event.preventDefault();
|
||||||
|
//check which column to convert.
|
||||||
|
sender = $(sender);
|
||||||
|
if (sender.attr("data-gas") == "consumption") {
|
||||||
|
switch (sender.attr("data-unit")) {
|
||||||
|
case "US gal":
|
||||||
|
convertGasConsumptionUnits("US gal", "l", true);
|
||||||
|
break;
|
||||||
|
case "l":
|
||||||
|
convertGasConsumptionUnits("l", "imp gal", true);
|
||||||
|
break;
|
||||||
|
case "imp gal":
|
||||||
|
convertGasConsumptionUnits("imp gal", "US gal", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (sender.attr("data-gas") == "fueleconomy") {
|
||||||
|
switch (sender.attr("data-unit")) {
|
||||||
|
case "l/100km":
|
||||||
|
convertFuelMileageUnits("l/100km", "km/l", true);
|
||||||
|
break;
|
||||||
|
case "km/l":
|
||||||
|
convertFuelMileageUnits("km/l", "l/100km", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
$.get('/Vehicle/GetAddNotePartialView', function (data) {
|
$.get('/Vehicle/GetAddNotePartialView', function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$("#noteModalContent").html(data);
|
$("#noteModalContent").html(data);
|
||||||
|
initTagSelector($("#noteRecordTag"));
|
||||||
$('#noteModal').modal('show');
|
$('#noteModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -10,7 +11,13 @@ function showEditNoteModal(noteId) {
|
|||||||
$.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) {
|
$.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$("#noteModalContent").html(data);
|
$("#noteModalContent").html(data);
|
||||||
|
initTagSelector($("#noteRecordTag"));
|
||||||
$('#noteModal').modal('show');
|
$('#noteModal').modal('show');
|
||||||
|
$('#noteModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("noteTextArea");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,6 +75,7 @@ function getAndValidateNoteValues() {
|
|||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var noteId = getNoteModelData().id;
|
var noteId = getNoteModelData().id;
|
||||||
var noteIsPinned = $("#noteIsPinned").is(":checked");
|
var noteIsPinned = $("#noteIsPinned").is(":checked");
|
||||||
|
var noteTags = $("#noteRecordTag").val();
|
||||||
//validation
|
//validation
|
||||||
var hasError = false;
|
var hasError = false;
|
||||||
if (noteDescription.trim() == '') { //eliminates whitespace.
|
if (noteDescription.trim() == '') { //eliminates whitespace.
|
||||||
@@ -88,6 +96,7 @@ function getAndValidateNoteValues() {
|
|||||||
vehicleId: vehicleId,
|
vehicleId: vehicleId,
|
||||||
description: noteDescription,
|
description: noteDescription,
|
||||||
noteText: noteText,
|
noteText: noteText,
|
||||||
pinned: noteIsPinned
|
pinned: noteIsPinned,
|
||||||
|
tags: noteTags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
$("#odometerRecordModalContent").html(data);
|
$("#odometerRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#odometerRecordDate'));
|
initDatePicker($('#odometerRecordDate'));
|
||||||
|
initTagSelector($("#odometerRecordTag"));
|
||||||
$('#odometerRecordModal').modal('show');
|
$('#odometerRecordModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -14,7 +15,13 @@ function showEditOdometerRecordModal(odometerRecordId) {
|
|||||||
$("#odometerRecordModalContent").html(data);
|
$("#odometerRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#odometerRecordDate'));
|
initDatePicker($('#odometerRecordDate'));
|
||||||
|
initTagSelector($("#odometerRecordTag"));
|
||||||
$('#odometerRecordModal').modal('show');
|
$('#odometerRecordModal').modal('show');
|
||||||
|
$('#odometerRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("odometerRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,8 +78,9 @@ function saveOdometerRecordToVehicle(isEdit) {
|
|||||||
}
|
}
|
||||||
function getAndValidateOdometerRecordValues() {
|
function getAndValidateOdometerRecordValues() {
|
||||||
var serviceDate = $("#odometerRecordDate").val();
|
var serviceDate = $("#odometerRecordDate").val();
|
||||||
var serviceMileage = $("#odometerRecordMileage").val();
|
var serviceMileage = parseInt(globalParseFloat($("#odometerRecordMileage").val())).toString();
|
||||||
var serviceNotes = $("#odometerRecordNotes").val();
|
var serviceNotes = $("#odometerRecordNotes").val();
|
||||||
|
var serviceTags = $("#odometerRecordTag").val();
|
||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var odometerRecordId = getOdometerRecordModelData().id;
|
var odometerRecordId = getOdometerRecordModelData().id;
|
||||||
//validation
|
//validation
|
||||||
@@ -96,6 +104,7 @@ function getAndValidateOdometerRecordValues() {
|
|||||||
date: serviceDate,
|
date: serviceDate,
|
||||||
mileage: serviceMileage,
|
mileage: serviceMileage,
|
||||||
notes: serviceNotes,
|
notes: serviceNotes,
|
||||||
|
tags: serviceTags,
|
||||||
files: uploadedFiles
|
files: uploadedFiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,11 @@ function showEditPlanRecordModal(planRecordId) {
|
|||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#planRecordDate'));
|
initDatePicker($('#planRecordDate'));
|
||||||
$('#planRecordModal').modal('show');
|
$('#planRecordModal').modal('show');
|
||||||
|
$('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("planRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,46 @@
|
|||||||
$("#reminderRecordModalContent").html(data);
|
$("#reminderRecordModalContent").html(data);
|
||||||
initDatePicker($('#reminderDate'), true);
|
initDatePicker($('#reminderDate'), true);
|
||||||
$("#reminderRecordModal").modal("show");
|
$("#reminderRecordModal").modal("show");
|
||||||
|
$('#reminderRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("reminderNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function hideAddReminderRecordModal() {
|
function hideAddReminderRecordModal() {
|
||||||
$('#reminderRecordModal').modal('hide');
|
$('#reminderRecordModal').modal('hide');
|
||||||
}
|
}
|
||||||
|
function checkCustomMileageInterval() {
|
||||||
|
var selectedValue = $("#reminderRecurringMileage").val();
|
||||||
|
if (selectedValue == "Other") {
|
||||||
|
$("#workAroundInput").show();
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Specify Custom Mileage Interval',
|
||||||
|
html: `
|
||||||
|
<input type="text" id="inputCustomMileage" class="swal2-input" placeholder="Mileage">
|
||||||
|
`,
|
||||||
|
confirmButtonText: 'Set',
|
||||||
|
focusConfirm: false,
|
||||||
|
preConfirm: () => {
|
||||||
|
const customMileage = $("#inputCustomMileage").val();
|
||||||
|
if (!customMileage || isNaN(parseInt(customMileage)) || parseInt(customMileage) <= 0) {
|
||||||
|
Swal.showValidationMessage(`Please enter a valid number`);
|
||||||
|
}
|
||||||
|
return { customMileage }
|
||||||
|
},
|
||||||
|
}).then(function (result) {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
customMileageInterval = result.value.customMileage;
|
||||||
|
$("option[value='Other']").text(`Other: ${result.value.customMileage}`);
|
||||||
|
} else {
|
||||||
|
$("#reminderRecurringMileage").val(getReminderRecordModelData().mileageInterval);
|
||||||
|
}
|
||||||
|
$("#workAroundInput").hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
function deleteReminderRecord(reminderRecordId, e) {
|
function deleteReminderRecord(reminderRecordId, e) {
|
||||||
if (e != undefined) {
|
if (e != undefined) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -96,13 +130,14 @@ function markDoneReminderRecord(reminderRecordId, e) {
|
|||||||
|
|
||||||
function getAndValidateReminderRecordValues() {
|
function getAndValidateReminderRecordValues() {
|
||||||
var reminderDate = $("#reminderDate").val();
|
var reminderDate = $("#reminderDate").val();
|
||||||
var reminderMileage = $("#reminderMileage").val();
|
var reminderMileage = parseInt(globalParseFloat($("#reminderMileage").val())).toString();
|
||||||
var reminderDescription = $("#reminderDescription").val();
|
var reminderDescription = $("#reminderDescription").val();
|
||||||
var reminderNotes = $("#reminderNotes").val();
|
var reminderNotes = $("#reminderNotes").val();
|
||||||
var reminderOption = $('#reminderOptions input:radio:checked').val();
|
var reminderOption = $('#reminderOptions input:radio:checked').val();
|
||||||
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
|
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
|
||||||
var reminderRecurringMonth = $("#reminderRecurringMonth").val();
|
var reminderRecurringMonth = $("#reminderRecurringMonth").val();
|
||||||
var reminderRecurringMileage = $("#reminderRecurringMileage").val();
|
var reminderRecurringMileage = $("#reminderRecurringMileage").val();
|
||||||
|
var reminderCustomMileageInterval = customMileageInterval;
|
||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var reminderId = getReminderRecordModelData().id;
|
var reminderId = getReminderRecordModelData().id;
|
||||||
//validation
|
//validation
|
||||||
@@ -149,6 +184,7 @@ function getAndValidateReminderRecordValues() {
|
|||||||
metric: reminderOption,
|
metric: reminderOption,
|
||||||
isRecurring: reminderIsRecurring,
|
isRecurring: reminderIsRecurring,
|
||||||
reminderMileageInterval: reminderRecurringMileage,
|
reminderMileageInterval: reminderRecurringMileage,
|
||||||
reminderMonthInterval: reminderRecurringMonth
|
reminderMonthInterval: reminderRecurringMonth,
|
||||||
|
customMileageInterval: customMileageInterval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,4 +74,60 @@ function refreshCollaborators() {
|
|||||||
$.get(`/Vehicle/GetCollaboratorsForVehicle?vehicleId=${vehicleId}`, function (data) {
|
$.get(`/Vehicle/GetCollaboratorsForVehicle?vehicleId=${vehicleId}`, function (data) {
|
||||||
$("#collaboratorContent").html(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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
$("#serviceRecordModalContent").html(data);
|
$("#serviceRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#serviceRecordDate'));
|
initDatePicker($('#serviceRecordDate'));
|
||||||
|
initTagSelector($("#serviceRecordTag"));
|
||||||
$('#serviceRecordModal').modal('show');
|
$('#serviceRecordModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -14,7 +15,13 @@ function showEditServiceRecordModal(serviceRecordId) {
|
|||||||
$("#serviceRecordModalContent").html(data);
|
$("#serviceRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#serviceRecordDate'));
|
initDatePicker($('#serviceRecordDate'));
|
||||||
|
initTagSelector($("#serviceRecordTag"));
|
||||||
$('#serviceRecordModal').modal('show');
|
$('#serviceRecordModal').modal('show');
|
||||||
|
$('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("serviceRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,10 +78,11 @@ function saveServiceRecordToVehicle(isEdit) {
|
|||||||
}
|
}
|
||||||
function getAndValidateServiceRecordValues() {
|
function getAndValidateServiceRecordValues() {
|
||||||
var serviceDate = $("#serviceRecordDate").val();
|
var serviceDate = $("#serviceRecordDate").val();
|
||||||
var serviceMileage = $("#serviceRecordMileage").val();
|
var serviceMileage = parseInt(globalParseFloat($("#serviceRecordMileage").val())).toString();
|
||||||
var serviceDescription = $("#serviceRecordDescription").val();
|
var serviceDescription = $("#serviceRecordDescription").val();
|
||||||
var serviceCost = $("#serviceRecordCost").val();
|
var serviceCost = $("#serviceRecordCost").val();
|
||||||
var serviceNotes = $("#serviceRecordNotes").val();
|
var serviceNotes = $("#serviceRecordNotes").val();
|
||||||
|
var serviceTags = $("#serviceRecordTag").val();
|
||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var serviceRecordId = getServiceRecordModelData().id;
|
var serviceRecordId = getServiceRecordModelData().id;
|
||||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||||
@@ -115,6 +123,7 @@ function getAndValidateServiceRecordValues() {
|
|||||||
notes: serviceNotes,
|
notes: serviceNotes,
|
||||||
files: uploadedFiles,
|
files: uploadedFiles,
|
||||||
supplies: selectedSupplies,
|
supplies: selectedSupplies,
|
||||||
|
tags: serviceTags,
|
||||||
addReminderRecord: addReminderRecord
|
addReminderRecord: addReminderRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,10 @@ function saveVehicle(isEdit) {
|
|||||||
var vehicleYear = $("#inputYear").val();
|
var vehicleYear = $("#inputYear").val();
|
||||||
var vehicleMake = $("#inputMake").val();
|
var vehicleMake = $("#inputMake").val();
|
||||||
var vehicleModel = $("#inputModel").val();
|
var vehicleModel = $("#inputModel").val();
|
||||||
|
var vehicleTags = $("#inputTag").val();
|
||||||
var vehicleLicensePlate = $("#inputLicensePlate").val();
|
var vehicleLicensePlate = $("#inputLicensePlate").val();
|
||||||
var vehicleIsElectric = $("#inputIsElectric").is(":checked");
|
var vehicleIsElectric = $("#inputIsElectric").is(":checked");
|
||||||
|
var vehicleUseHours = $("#inputUseHours").is(":checked");
|
||||||
//validate
|
//validate
|
||||||
var hasError = false;
|
var hasError = false;
|
||||||
if (vehicleYear.trim() == '' || parseInt(vehicleYear) < 1900) {
|
if (vehicleYear.trim() == '' || parseInt(vehicleYear) < 1900) {
|
||||||
@@ -74,7 +76,9 @@ function saveVehicle(isEdit) {
|
|||||||
make: vehicleMake,
|
make: vehicleMake,
|
||||||
model: vehicleModel,
|
model: vehicleModel,
|
||||||
licensePlate: vehicleLicensePlate,
|
licensePlate: vehicleLicensePlate,
|
||||||
isElectric: vehicleIsElectric
|
isElectric: vehicleIsElectric,
|
||||||
|
tags: vehicleTags,
|
||||||
|
useHours: vehicleUseHours
|
||||||
}, function (data) {
|
}, function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!isEdit) {
|
if (!isEdit) {
|
||||||
@@ -108,6 +112,10 @@ function uploadFileAsync(event) {
|
|||||||
if (response.trim() != '') {
|
if (response.trim() != '') {
|
||||||
uploadedFile = response;
|
uploadedFile = response;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
sloader.hide();
|
||||||
|
errorToast("An error has occurred, please check the file size and try again later.")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,6 +139,15 @@ function initDatePicker(input, futureOnly) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function initTagSelector(input, noDataList) {
|
||||||
|
if (noDataList) {
|
||||||
|
input.tagsinput({
|
||||||
|
useDataList: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
input.tagsinput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showMobileNav() {
|
function showMobileNav() {
|
||||||
$(".lubelogger-mobile-nav").addClass("lubelogger-mobile-nav-show");
|
$(".lubelogger-mobile-nav").addClass("lubelogger-mobile-nav-show");
|
||||||
@@ -154,4 +171,93 @@ function setDebounce(callBack) {
|
|||||||
debounce = setTimeout(function () {
|
debounce = setTimeout(function () {
|
||||||
callBack();
|
callBack();
|
||||||
}, 1000);
|
}, 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);
|
||||||
|
filterTable(tabName, $(".tagfilter.bg-primary").get(0), true);
|
||||||
|
} 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);
|
||||||
|
filterTable(tabName, $(".tagfilter.bg-primary").get(0), true);
|
||||||
|
}
|
||||||
|
function filterTable(tabName, sender, isSort) {
|
||||||
|
var rowData = $(`#${tabName} table tbody tr`);
|
||||||
|
if (sender == undefined) {
|
||||||
|
rowData.removeClass('override-hide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var tagName = sender.textContent;
|
||||||
|
//check for other applied filters
|
||||||
|
if ($(sender).hasClass("bg-primary")) {
|
||||||
|
if (!isSort) {
|
||||||
|
rowData.removeClass('override-hide');
|
||||||
|
$(sender).removeClass('bg-primary');
|
||||||
|
$(sender).addClass('bg-secondary');
|
||||||
|
} else {
|
||||||
|
rowData.addClass('override-hide');
|
||||||
|
$(`[data-tags~='${tagName}']`).removeClass('override-hide');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//hide table rows.
|
||||||
|
rowData.addClass('override-hide');
|
||||||
|
$(`[data-tags~='${tagName}']`).removeClass('override-hide');
|
||||||
|
if ($(".tagfilter.bg-primary").length > 0) {
|
||||||
|
//disabling other filters
|
||||||
|
$(".tagfilter.bg-primary").addClass('bg-secondary');
|
||||||
|
$(".tagfilter.bg-primary").removeClass('bg-primary');
|
||||||
|
}
|
||||||
|
$(sender).addClass('bg-primary');
|
||||||
|
$(sender).removeClass('bg-secondary');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,11 @@ function showEditSupplyRecordModal(supplyRecordId) {
|
|||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#supplyRecordDate'));
|
initDatePicker($('#supplyRecordDate'));
|
||||||
$('#supplyRecordModal').modal('show');
|
$('#supplyRecordModal').modal('show');
|
||||||
|
$('#supplyRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("supplyRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
$("#taxRecordModalContent").html(data);
|
$("#taxRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#taxRecordDate'));
|
initDatePicker($('#taxRecordDate'));
|
||||||
|
initTagSelector($("#taxRecordTag"));
|
||||||
$('#taxRecordModal').modal('show');
|
$('#taxRecordModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -14,7 +15,13 @@ function showEditTaxRecordModal(taxRecordId) {
|
|||||||
$("#taxRecordModalContent").html(data);
|
$("#taxRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#taxRecordDate'));
|
initDatePicker($('#taxRecordDate'));
|
||||||
|
initTagSelector($("#taxRecordTag"));
|
||||||
$('#taxRecordModal').modal('show');
|
$('#taxRecordModal').modal('show');
|
||||||
|
$('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("taxRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,6 +93,7 @@ function getAndValidateTaxRecordValues() {
|
|||||||
var taxRecordId = getTaxRecordModelData().id;
|
var taxRecordId = getTaxRecordModelData().id;
|
||||||
var taxIsRecurring = $("#taxIsRecurring").is(":checked");
|
var taxIsRecurring = $("#taxIsRecurring").is(":checked");
|
||||||
var taxRecurringMonth = $("#taxRecurringMonth").val();
|
var taxRecurringMonth = $("#taxRecurringMonth").val();
|
||||||
|
var taxTags = $("#taxRecordTag").val();
|
||||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||||
//validation
|
//validation
|
||||||
var hasError = false;
|
var hasError = false;
|
||||||
@@ -117,6 +125,7 @@ function getAndValidateTaxRecordValues() {
|
|||||||
notes: taxNotes,
|
notes: taxNotes,
|
||||||
isRecurring: taxIsRecurring,
|
isRecurring: taxIsRecurring,
|
||||||
recurringInterval: taxRecurringMonth,
|
recurringInterval: taxRecurringMonth,
|
||||||
|
tags: taxTags,
|
||||||
files: uploadedFiles,
|
files: uploadedFiles,
|
||||||
addReminderRecord: addReminderRecord
|
addReminderRecord: addReminderRecord
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
$("#upgradeRecordModalContent").html(data);
|
$("#upgradeRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#upgradeRecordDate'));
|
initDatePicker($('#upgradeRecordDate'));
|
||||||
|
initTagSelector($("#upgradeRecordTag"));
|
||||||
$('#upgradeRecordModal').modal('show');
|
$('#upgradeRecordModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -14,7 +15,13 @@ function showEditUpgradeRecordModal(upgradeRecordId) {
|
|||||||
$("#upgradeRecordModalContent").html(data);
|
$("#upgradeRecordModalContent").html(data);
|
||||||
//initiate datepicker
|
//initiate datepicker
|
||||||
initDatePicker($('#upgradeRecordDate'));
|
initDatePicker($('#upgradeRecordDate'));
|
||||||
|
initTagSelector($("#upgradeRecordTag"));
|
||||||
$('#upgradeRecordModal').modal('show');
|
$('#upgradeRecordModal').modal('show');
|
||||||
|
$('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||||
|
if (getGlobalConfig().useMarkDown) {
|
||||||
|
toggleMarkDownOverlay("upgradeRecordNotes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,10 +78,11 @@ function saveUpgradeRecordToVehicle(isEdit) {
|
|||||||
}
|
}
|
||||||
function getAndValidateUpgradeRecordValues() {
|
function getAndValidateUpgradeRecordValues() {
|
||||||
var upgradeDate = $("#upgradeRecordDate").val();
|
var upgradeDate = $("#upgradeRecordDate").val();
|
||||||
var upgradeMileage = $("#upgradeRecordMileage").val();
|
var upgradeMileage = parseInt(globalParseFloat($("#upgradeRecordMileage").val())).toString();
|
||||||
var upgradeDescription = $("#upgradeRecordDescription").val();
|
var upgradeDescription = $("#upgradeRecordDescription").val();
|
||||||
var upgradeCost = $("#upgradeRecordCost").val();
|
var upgradeCost = $("#upgradeRecordCost").val();
|
||||||
var upgradeNotes = $("#upgradeRecordNotes").val();
|
var upgradeNotes = $("#upgradeRecordNotes").val();
|
||||||
|
var upgradeTags = $("#upgradeRecordTag").val();
|
||||||
var vehicleId = GetVehicleId().vehicleId;
|
var vehicleId = GetVehicleId().vehicleId;
|
||||||
var upgradeRecordId = getUpgradeRecordModelData().id;
|
var upgradeRecordId = getUpgradeRecordModelData().id;
|
||||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||||
@@ -115,6 +123,7 @@ function getAndValidateUpgradeRecordValues() {
|
|||||||
notes: upgradeNotes,
|
notes: upgradeNotes,
|
||||||
files: uploadedFiles,
|
files: uploadedFiles,
|
||||||
supplies: selectedSupplies,
|
supplies: selectedSupplies,
|
||||||
|
tags: upgradeTags,
|
||||||
addReminderRecord: addReminderRecord
|
addReminderRecord: addReminderRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +214,7 @@ function editVehicle(vehicleId) {
|
|||||||
$.get(`/Vehicle/GetEditVehiclePartialViewById?vehicleId=${vehicleId}`, function (data) {
|
$.get(`/Vehicle/GetEditVehiclePartialViewById?vehicleId=${vehicleId}`, function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$("#editVehicleModalContent").html(data);
|
$("#editVehicleModalContent").html(data);
|
||||||
|
initTagSelector($("#inputTag"), true);
|
||||||
$('#editVehicleModal').modal('show');
|
$('#editVehicleModal').modal('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -278,6 +279,10 @@ function uploadVehicleFilesAsync(event) {
|
|||||||
if (response.length > 0) {
|
if (response.length > 0) {
|
||||||
uploadedFiles.push.apply(uploadedFiles, response);
|
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 +318,11 @@ function getVehicleHaveImportantReminders(vehicleId) {
|
|||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
function printTab() {
|
||||||
|
setTimeout(function () {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
function deleteFileFromUploadedFiles(fileLocation, event) {
|
function deleteFileFromUploadedFiles(fileLocation, event) {
|
||||||
event.parentElement.parentElement.parentElement.remove();
|
event.parentElement.parentElement.parentElement.remove();
|
||||||
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
|
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
|
||||||
@@ -349,4 +358,82 @@ function saveScrollPosition() {
|
|||||||
function restoreScrollPosition() {
|
function restoreScrollPosition() {
|
||||||
$(".vehicleDetailTabContainer").scrollTop(scrollPosition);
|
$(".vehicleDetailTabContainer").scrollTop(scrollPosition);
|
||||||
scrollPosition = 0;
|
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);
|
||||||
}
|
}
|
||||||
21
wwwroot/lib/bootstrap-tagsinput/bootstrap-tagsinput.css
vendored
Normal file
21
wwwroot/lib/bootstrap-tagsinput/bootstrap-tagsinput.css
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.bootstrap-tagsinput {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput input {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
max-width: inherit;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput input:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag [data-role="remove"] {
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
669
wwwroot/lib/bootstrap-tagsinput/bootstrap-tagsinput.js
vendored
Normal file
669
wwwroot/lib/bootstrap-tagsinput/bootstrap-tagsinput.js
vendored
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var defaultOptions = {
|
||||||
|
tagClass: function(item) {
|
||||||
|
return 'label label-info';
|
||||||
|
},
|
||||||
|
itemValue: function(item) {
|
||||||
|
return item ? item.toString() : item;
|
||||||
|
},
|
||||||
|
itemText: function(item) {
|
||||||
|
return this.itemValue(item);
|
||||||
|
},
|
||||||
|
itemTitle: function(item) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
freeInput: true,
|
||||||
|
addOnBlur: true,
|
||||||
|
maxTags: undefined,
|
||||||
|
maxChars: undefined,
|
||||||
|
confirmKeys: [13,32],
|
||||||
|
delimiter: ',',
|
||||||
|
delimiterRegex: null,
|
||||||
|
cancelConfirmKeysOnEmpty: true,
|
||||||
|
onTagExists: function(item, $tag) {
|
||||||
|
$tag.hide().fadeIn();
|
||||||
|
},
|
||||||
|
trimValue: false,
|
||||||
|
allowDuplicates: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor function
|
||||||
|
*/
|
||||||
|
function TagsInput(element, options) {
|
||||||
|
this.itemsArray = [];
|
||||||
|
this.$element = $(element);
|
||||||
|
this.$element.hide();
|
||||||
|
|
||||||
|
this.isSelect = (element.tagName === 'SELECT');
|
||||||
|
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
||||||
|
this.objectItems = options && options.itemValue;
|
||||||
|
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
||||||
|
this.inputSize = Math.max(1, this.placeholderText.length);
|
||||||
|
|
||||||
|
var useDataList = true;
|
||||||
|
if (options != undefined && !options.useDataList) {
|
||||||
|
useDataList = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$container = $('<div class="form-control bootstrap-tagsinput"></div>');
|
||||||
|
this.$input = $(`<input type="text" ${!useDataList ? "" : "list='tagList'"}placeholder="` + this.placeholderText + '"/>').appendTo(this.$container);
|
||||||
|
|
||||||
|
this.$element.before(this.$container);
|
||||||
|
|
||||||
|
this.build(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagsInput.prototype = {
|
||||||
|
constructor: TagsInput,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
|
||||||
|
* updating the elements val()
|
||||||
|
*/
|
||||||
|
add: function(item, dontPushVal, options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ignore falsey values, except false
|
||||||
|
if (item !== false && !item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Trim value
|
||||||
|
if (typeof item === "string" && self.options.trimValue) {
|
||||||
|
item = $.trim(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error when trying to add an object while the itemValue option was not set
|
||||||
|
if (typeof item === "object" && !self.objectItems)
|
||||||
|
throw("Can't add objects when itemValue option is not set");
|
||||||
|
|
||||||
|
// Ignore strings only containg whitespace
|
||||||
|
if (item.toString().match(/^\s*$/))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If SELECT but not multiple, remove current tag
|
||||||
|
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
|
||||||
|
self.remove(self.itemsArray[0]);
|
||||||
|
|
||||||
|
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
|
||||||
|
var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
|
||||||
|
var items = item.split(delimiter);
|
||||||
|
if (items.length > 1) {
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
this.add(items[i], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemValue = self.options.itemValue(item),
|
||||||
|
itemText = self.options.itemText(item),
|
||||||
|
tagClass = self.options.tagClass(item),
|
||||||
|
itemTitle = self.options.itemTitle(item);
|
||||||
|
|
||||||
|
// Ignore items allready added
|
||||||
|
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
|
||||||
|
if (existing && !self.options.allowDuplicates) {
|
||||||
|
// Invoke onTagExists
|
||||||
|
if (self.options.onTagExists) {
|
||||||
|
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
|
||||||
|
self.options.onTagExists(item, $existingTag);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if length greater than limit
|
||||||
|
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// raise beforeItemAdd arg
|
||||||
|
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
|
||||||
|
self.$element.trigger(beforeItemAddEvent);
|
||||||
|
if (beforeItemAddEvent.cancel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// register item in internal array and map
|
||||||
|
self.itemsArray.push(item);
|
||||||
|
|
||||||
|
// add a tag element
|
||||||
|
|
||||||
|
var $tag = $('<span class="tag badge rounded-pill text-bg-primary ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"><i class="bi bi-x"></i></span></span>');
|
||||||
|
$tag.data('item', item);
|
||||||
|
self.findInputWrapper().before($tag);
|
||||||
|
$tag.after(' ');
|
||||||
|
|
||||||
|
// add <option /> if item represents a value not present in one of the <select />'s options
|
||||||
|
if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) {
|
||||||
|
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
|
||||||
|
$option.data('item', item);
|
||||||
|
$option.attr('value', itemValue);
|
||||||
|
self.$element.append($option);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
|
||||||
|
// Add class when reached maxTags
|
||||||
|
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
|
||||||
|
self.$container.addClass('bootstrap-tagsinput-max');
|
||||||
|
|
||||||
|
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given item. Pass true to dontPushVal to prevent updating the
|
||||||
|
* elements val()
|
||||||
|
*/
|
||||||
|
remove: function(item, dontPushVal, options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self.objectItems) {
|
||||||
|
if (typeof item === "object")
|
||||||
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
|
||||||
|
else
|
||||||
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
|
||||||
|
|
||||||
|
item = item[item.length-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
|
||||||
|
self.$element.trigger(beforeItemRemoveEvent);
|
||||||
|
if (beforeItemRemoveEvent.cancel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
|
||||||
|
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
|
||||||
|
if($.inArray(item, self.itemsArray) !== -1)
|
||||||
|
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
|
||||||
|
// Remove class when reached maxTags
|
||||||
|
if (self.options.maxTags > self.itemsArray.length)
|
||||||
|
self.$container.removeClass('bootstrap-tagsinput-max');
|
||||||
|
|
||||||
|
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all items
|
||||||
|
*/
|
||||||
|
removeAll: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$('.tag', self.$container).remove();
|
||||||
|
$('option', self.$element).remove();
|
||||||
|
|
||||||
|
while(self.itemsArray.length > 0)
|
||||||
|
self.itemsArray.pop();
|
||||||
|
|
||||||
|
self.pushVal();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the tags so they match the text/value of their corresponding
|
||||||
|
* item.
|
||||||
|
*/
|
||||||
|
refresh: function() {
|
||||||
|
var self = this;
|
||||||
|
$('.tag', self.$container).each(function() {
|
||||||
|
var $tag = $(this),
|
||||||
|
item = $tag.data('item'),
|
||||||
|
itemValue = self.options.itemValue(item),
|
||||||
|
itemText = self.options.itemText(item),
|
||||||
|
tagClass = self.options.tagClass(item);
|
||||||
|
|
||||||
|
// Update tag's class and inner text
|
||||||
|
$tag.attr('class', null);
|
||||||
|
$tag.addClass('tag ' + htmlEncode(tagClass));
|
||||||
|
$tag.contents().filter(function() {
|
||||||
|
return this.nodeType == 3;
|
||||||
|
})[0].nodeValue = htmlEncode(itemText);
|
||||||
|
|
||||||
|
if (self.isSelect) {
|
||||||
|
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
|
||||||
|
option.attr('value', itemValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the items added as tags
|
||||||
|
*/
|
||||||
|
items: function() {
|
||||||
|
return this.itemsArray;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assembly value by retrieving the value of each item, and set it on the
|
||||||
|
* element.
|
||||||
|
*/
|
||||||
|
pushVal: function() {
|
||||||
|
var self = this,
|
||||||
|
val = $.map(self.items(), function(item) {
|
||||||
|
return self.options.itemValue(item).toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.$element.val(val, true).trigger('change');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the tags input behaviour on the element
|
||||||
|
*/
|
||||||
|
build: function(options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.options = $.extend({}, defaultOptions, options);
|
||||||
|
// When itemValue is set, freeInput should always be false
|
||||||
|
if (self.objectItems)
|
||||||
|
self.options.freeInput = false;
|
||||||
|
|
||||||
|
makeOptionItemFunction(self.options, 'itemValue');
|
||||||
|
makeOptionItemFunction(self.options, 'itemText');
|
||||||
|
makeOptionFunction(self.options, 'tagClass');
|
||||||
|
|
||||||
|
// Typeahead Bootstrap version 2.3.2
|
||||||
|
if (self.options.typeahead) {
|
||||||
|
var typeahead = self.options.typeahead || {};
|
||||||
|
|
||||||
|
makeOptionFunction(typeahead, 'source');
|
||||||
|
|
||||||
|
self.$input.typeahead($.extend({}, typeahead, {
|
||||||
|
source: function (query, process) {
|
||||||
|
function processItems(items) {
|
||||||
|
var texts = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var text = self.options.itemText(items[i]);
|
||||||
|
map[text] = items[i];
|
||||||
|
texts.push(text);
|
||||||
|
}
|
||||||
|
process(texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map = {};
|
||||||
|
var map = this.map,
|
||||||
|
data = typeahead.source(query);
|
||||||
|
|
||||||
|
if ($.isFunction(data.success)) {
|
||||||
|
// support for Angular callbacks
|
||||||
|
data.success(processItems);
|
||||||
|
} else if ($.isFunction(data.then)) {
|
||||||
|
// support for Angular promises
|
||||||
|
data.then(processItems);
|
||||||
|
} else {
|
||||||
|
// support for functions and jquery promises
|
||||||
|
$.when(data)
|
||||||
|
.then(processItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updater: function (text) {
|
||||||
|
self.add(this.map[text]);
|
||||||
|
return this.map[text];
|
||||||
|
},
|
||||||
|
matcher: function (text) {
|
||||||
|
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
|
||||||
|
},
|
||||||
|
sorter: function (texts) {
|
||||||
|
return texts.sort();
|
||||||
|
},
|
||||||
|
highlighter: function (text) {
|
||||||
|
var regex = new RegExp( '(' + this.query + ')', 'gi' );
|
||||||
|
return text.replace( regex, "<strong>$1</strong>" );
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeahead.js
|
||||||
|
if (self.options.typeaheadjs) {
|
||||||
|
var typeaheadConfig = null;
|
||||||
|
var typeaheadDatasets = {};
|
||||||
|
|
||||||
|
// Determine if main configurations were passed or simply a dataset
|
||||||
|
var typeaheadjs = self.options.typeaheadjs;
|
||||||
|
if ($.isArray(typeaheadjs)) {
|
||||||
|
typeaheadConfig = typeaheadjs[0];
|
||||||
|
typeaheadDatasets = typeaheadjs[1];
|
||||||
|
} else {
|
||||||
|
typeaheadDatasets = typeaheadjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) {
|
||||||
|
if (typeaheadDatasets.valueKey)
|
||||||
|
self.add(datum[typeaheadDatasets.valueKey]);
|
||||||
|
else
|
||||||
|
self.add(datum);
|
||||||
|
self.$input.typeahead('val', '');
|
||||||
|
}, self));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.$container.on('click', $.proxy(function(event) {
|
||||||
|
if (! self.$element.attr('disabled')) {
|
||||||
|
self.$input.removeAttr('disabled');
|
||||||
|
}
|
||||||
|
self.$input.focus();
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
if (self.options.addOnBlur && self.options.freeInput) {
|
||||||
|
self.$input.on('focusout', $.proxy(function(event) {
|
||||||
|
// HACK: only process on focusout when no typeahead opened, to
|
||||||
|
// avoid adding the typeahead text as tag
|
||||||
|
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
|
||||||
|
self.add(self.$input.val());
|
||||||
|
self.$input.val('');
|
||||||
|
}
|
||||||
|
}, self));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
||||||
|
var $input = $(event.target),
|
||||||
|
$inputWrapper = self.findInputWrapper();
|
||||||
|
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
self.$input.attr('disabled', 'disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.which) {
|
||||||
|
// BACKSPACE
|
||||||
|
case 8:
|
||||||
|
if (doGetCaretPosition($input[0]) === 0) {
|
||||||
|
var prev = $inputWrapper.prev();
|
||||||
|
if (prev.length) {
|
||||||
|
self.remove(prev.data('item'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
case 46:
|
||||||
|
if (doGetCaretPosition($input[0]) === 0) {
|
||||||
|
var next = $inputWrapper.next();
|
||||||
|
if (next.length) {
|
||||||
|
self.remove(next.data('item'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// LEFT ARROW
|
||||||
|
case 37:
|
||||||
|
// Try to move the input before the previous tag
|
||||||
|
var $prevTag = $inputWrapper.prev();
|
||||||
|
if ($input.val().length === 0 && $prevTag[0]) {
|
||||||
|
$prevTag.before($inputWrapper);
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// RIGHT ARROW
|
||||||
|
case 39:
|
||||||
|
// Try to move the input after the next tag
|
||||||
|
var $nextTag = $inputWrapper.next();
|
||||||
|
if ($input.val().length === 0 && $nextTag[0]) {
|
||||||
|
$nextTag.after($inputWrapper);
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal input's size
|
||||||
|
var textLength = $input.val().length,
|
||||||
|
wordSpace = Math.ceil(textLength / 5),
|
||||||
|
size = textLength + wordSpace + 1;
|
||||||
|
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
self.$container.on('input', 'input', $.proxy(function (event) {
|
||||||
|
if (event.originalEvent.data == undefined) {
|
||||||
|
var $input = $(event.target);
|
||||||
|
var text = $input.val(),
|
||||||
|
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
||||||
|
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
||||||
|
//check if confirm keys are in input and then replace them.
|
||||||
|
text = text.replace(String.fromCharCode(event.which), "")
|
||||||
|
// Only attempt to add a tag if there is data in the field
|
||||||
|
if (text.length !== 0) {
|
||||||
|
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
||||||
|
$input.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field is empty, let the event triggered fire as usual
|
||||||
|
if (self.options.cancelConfirmKeysOnEmpty === false) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var textLength = $input.val().length,
|
||||||
|
wordSpace = Math.ceil(textLength / 5),
|
||||||
|
size = textLength + wordSpace + 1;
|
||||||
|
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.$container.on('keypress', 'input', $.proxy(function(event) {
|
||||||
|
var $input = $(event.target);
|
||||||
|
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
self.$input.attr('disabled', 'disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var text = $input.val(),
|
||||||
|
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
||||||
|
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
||||||
|
//check if confirm keys are in input and then replace them.
|
||||||
|
text = text.replace(String.fromCharCode(event.which), "")
|
||||||
|
// Only attempt to add a tag if there is data in the field
|
||||||
|
if (text.length !== 0) {
|
||||||
|
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
||||||
|
$input.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field is empty, let the event triggered fire as usual
|
||||||
|
if (self.options.cancelConfirmKeysOnEmpty === false) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal input's size
|
||||||
|
var textLength = $input.val().length,
|
||||||
|
wordSpace = Math.ceil(textLength / 5),
|
||||||
|
size = textLength + wordSpace + 1;
|
||||||
|
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
// Remove icon clicked
|
||||||
|
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.remove($(event.target).closest('.tag').data('item'));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
// Only add existing value as tags when using strings as tags
|
||||||
|
if (self.options.itemValue === defaultOptions.itemValue) {
|
||||||
|
if (self.$element[0].tagName === 'INPUT') {
|
||||||
|
self.add(self.$element.val());
|
||||||
|
} else {
|
||||||
|
$('option', self.$element).each(function() {
|
||||||
|
self.add($(this).attr('value'), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all tagsinput behaviour and unregsiter all event handlers
|
||||||
|
*/
|
||||||
|
destroy: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Unbind events
|
||||||
|
self.$container.off('keypress', 'input');
|
||||||
|
self.$container.off('click', '[role=remove]');
|
||||||
|
|
||||||
|
self.$container.remove();
|
||||||
|
self.$element.removeData('tagsinput');
|
||||||
|
self.$element.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets focus on the tagsinput
|
||||||
|
*/
|
||||||
|
focus: function() {
|
||||||
|
this.$input.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal input element
|
||||||
|
*/
|
||||||
|
input: function() {
|
||||||
|
return this.$input;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the element which is wrapped around the internal input. This
|
||||||
|
* is normally the $container, but typeahead.js moves the $input element.
|
||||||
|
*/
|
||||||
|
findInputWrapper: function() {
|
||||||
|
var elt = this.$input[0],
|
||||||
|
container = this.$container[0];
|
||||||
|
while(elt && elt.parentNode !== container)
|
||||||
|
elt = elt.parentNode;
|
||||||
|
|
||||||
|
return $(elt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register JQuery plugin
|
||||||
|
*/
|
||||||
|
$.fn.tagsinput = function(arg1, arg2, arg3) {
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
this.each(function() {
|
||||||
|
var tagsinput = $(this).data('tagsinput');
|
||||||
|
// Initialize a new tags input
|
||||||
|
if (!tagsinput) {
|
||||||
|
tagsinput = new TagsInput(this, arg1);
|
||||||
|
$(this).data('tagsinput', tagsinput);
|
||||||
|
results.push(tagsinput);
|
||||||
|
|
||||||
|
if (this.tagName === 'SELECT') {
|
||||||
|
$('option', $(this)).attr('selected', 'selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init tags from $(this).val()
|
||||||
|
$(this).val($(this).val());
|
||||||
|
} else if (!arg1 && !arg2) {
|
||||||
|
// tagsinput already exists
|
||||||
|
// no function, trying to init
|
||||||
|
results.push(tagsinput);
|
||||||
|
} else if(tagsinput[arg1] !== undefined) {
|
||||||
|
// Invoke function on existing tags input
|
||||||
|
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
|
||||||
|
var retVal = tagsinput[arg1](arg2, null, arg3);
|
||||||
|
}else{
|
||||||
|
var retVal = tagsinput[arg1](arg2);
|
||||||
|
}
|
||||||
|
if (retVal !== undefined)
|
||||||
|
results.push(retVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( typeof arg1 == 'string') {
|
||||||
|
// Return the results from the invoked function calls
|
||||||
|
return results.length > 1 ? results : results[0];
|
||||||
|
} else {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.tagsinput.Constructor = TagsInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most options support both a string or number as well as a function as
|
||||||
|
* option value. This function makes sure that the option with the given
|
||||||
|
* key in the given options is wrapped in a function
|
||||||
|
*/
|
||||||
|
function makeOptionItemFunction(options, key) {
|
||||||
|
if (typeof options[key] !== 'function') {
|
||||||
|
var propertyName = options[key];
|
||||||
|
options[key] = function(item) { return item[propertyName]; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function makeOptionFunction(options, key) {
|
||||||
|
if (typeof options[key] !== 'function') {
|
||||||
|
var value = options[key];
|
||||||
|
options[key] = function() { return value; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HtmlEncodes the given value
|
||||||
|
*/
|
||||||
|
var htmlEncodeContainer = $('<div />');
|
||||||
|
function htmlEncode(value) {
|
||||||
|
if (value) {
|
||||||
|
return htmlEncodeContainer.text(value).html();
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of the caret in the given input field
|
||||||
|
* http://flightschool.acylt.com/devnotes/caret-position-woes/
|
||||||
|
*/
|
||||||
|
function doGetCaretPosition(oField) {
|
||||||
|
var iCaretPos = 0;
|
||||||
|
if (document.selection) {
|
||||||
|
oField.focus ();
|
||||||
|
var oSel = document.selection.createRange();
|
||||||
|
oSel.moveStart ('character', -oField.value.length);
|
||||||
|
iCaretPos = oSel.text.length;
|
||||||
|
} else if (oField.selectionStart || oField.selectionStart == '0') {
|
||||||
|
iCaretPos = oField.selectionStart;
|
||||||
|
}
|
||||||
|
return (iCaretPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean indicates whether user has pressed an expected key combination.
|
||||||
|
* @param object keyPressEvent: JavaScript event object, refer
|
||||||
|
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
||||||
|
* @param object lookupList: expected key combinations, as in:
|
||||||
|
* [13, {which: 188, shiftKey: true}]
|
||||||
|
*/
|
||||||
|
function keyCombinationInList(keyPressEvent, lookupList) {
|
||||||
|
var found = false;
|
||||||
|
$.each(lookupList, function (index, keyCombination) {
|
||||||
|
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyPressEvent.which === keyCombination.which) {
|
||||||
|
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
|
||||||
|
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
|
||||||
|
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
|
||||||
|
if (alt && shift && ctrl) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
})(window.jQuery);
|
||||||
129
wwwroot/lib/drawdown/drawdown.js
Normal file
129
wwwroot/lib/drawdown/drawdown.js
Normal 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 *> *([^]*?)(?=(\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(/^ *> */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, '<');
|
||||||
|
replace(rx_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();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user