Compare commits
54 Commits
| 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 |
@@ -1,20 +1,22 @@
|
||||
name: Docker Image To GHCR
|
||||
name: Build and Push Image to Dockerhub and GHCR
|
||||
|
||||
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: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -28,4 +30,6 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
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
|
||||
@@ -608,6 +608,15 @@ namespace CarCareTracker.Controllers
|
||||
var result = _gasRecordDataAccess.DeleteGasRecordById(gasRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit)
|
||||
{
|
||||
var currentConfig = _config.GetUserConfig(User);
|
||||
currentConfig.PreferredGasUnit = gasUnit;
|
||||
currentConfig.PreferredGasMileageUnit = fuelMileageUnit;
|
||||
var result = _config.SaveUserConfig(User, currentConfig);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Service Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
@@ -667,7 +676,8 @@ namespace CarCareTracker.Controllers
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
Files = result.Files,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_ServiceRecordModal", convertedResult);
|
||||
}
|
||||
@@ -736,7 +746,8 @@ namespace CarCareTracker.Controllers
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
Files = result.Files,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_CollisionRecordModal", convertedResult);
|
||||
}
|
||||
@@ -784,7 +795,8 @@ namespace CarCareTracker.Controllers
|
||||
IsRecurring = true,
|
||||
Notes = recurringFee.Notes,
|
||||
RecurringInterval = recurringFee.RecurringInterval,
|
||||
Files = recurringFee.Files
|
||||
Files = recurringFee.Files,
|
||||
Tags = recurringFee.Tags
|
||||
};
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee);
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(newRecurringFee);
|
||||
@@ -820,7 +832,8 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = result.VehicleId,
|
||||
IsRecurring = result.IsRecurring,
|
||||
RecurringInterval = result.RecurringInterval,
|
||||
Files = result.Files
|
||||
Files = result.Files,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_TaxRecordModal", convertedResult);
|
||||
}
|
||||
@@ -1072,6 +1085,7 @@ namespace CarCareTracker.Controllers
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
bool useMPG = _config.GetUserConfig(User).UseMPG;
|
||||
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
|
||||
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
|
||||
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);
|
||||
var averageMPG = "0";
|
||||
@@ -1080,7 +1094,19 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
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
|
||||
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
@@ -1309,7 +1335,8 @@ namespace CarCareTracker.Controllers
|
||||
Metric = result.Metric,
|
||||
IsRecurring = result.IsRecurring,
|
||||
ReminderMileageInterval = result.ReminderMileageInterval,
|
||||
ReminderMonthInterval = result.ReminderMonthInterval
|
||||
ReminderMonthInterval = result.ReminderMonthInterval,
|
||||
CustomMileageInterval = result.CustomMileageInterval
|
||||
};
|
||||
return PartialView("_ReminderRecordModal", convertedResult);
|
||||
}
|
||||
@@ -1378,7 +1405,8 @@ namespace CarCareTracker.Controllers
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
Files = result.Files,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_UpgradeRecordModal", convertedResult);
|
||||
}
|
||||
@@ -1685,7 +1713,8 @@ namespace CarCareTracker.Controllers
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
Files = result.Files,
|
||||
Tags = result.Tags
|
||||
};
|
||||
return PartialView("_OdometerRecordModal", convertedResult);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public enum ReminderMileageInterval
|
||||
{
|
||||
Other = 0,
|
||||
FiftyMiles = 50,
|
||||
OneHundredMiles = 100,
|
||||
FiveHundredMiles = 500,
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
UserConfig GetUserConfig(ClaimsPrincipal user);
|
||||
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
|
||||
string GetLogoUrl();
|
||||
public bool DeleteUserConfig(int userId);
|
||||
}
|
||||
public class ConfigHelper : IConfigHelper
|
||||
@@ -24,6 +25,15 @@ namespace CarCareTracker.Helper
|
||||
_userConfig = userConfig;
|
||||
_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)
|
||||
{
|
||||
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
@@ -98,6 +108,8 @@ namespace CarCareTracker.Helper
|
||||
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
|
||||
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
|
||||
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
|
||||
PreferredGasMileageUnit = _config[nameof(UserConfig.PreferredGasMileageUnit)],
|
||||
PreferredGasUnit = _config[nameof(UserConfig.PreferredGasUnit)],
|
||||
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
|
||||
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
|
||||
};
|
||||
|
||||
@@ -13,13 +13,14 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
var recordsToCalculateGallons = results.Where(x => x.MilesPerGallon > 0 || !x.IsFillToFull);
|
||||
var recordWithCalculatedMPG = recordsToCalculateGallons.Where(x => x.MilesPerGallon > 0);
|
||||
var minMileage = results.Min(x => x.Mileage);
|
||||
var mileageAdjustment = results.Where(x => x.MissedFuelUp).Sum(y => y.DeltaMileage);
|
||||
if (recordWithCalculatedMPG.Any())
|
||||
{
|
||||
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
|
||||
var minMileage = recordWithCalculatedMPG.Min(x => x.Mileage);
|
||||
var totalGallonsConsumed = recordsToCalculateGallons.Sum(x => x.Gallons);
|
||||
var deltaMileage = maxMileage - minMileage;
|
||||
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed;
|
||||
var deltaMileage = maxMileage - minMileage - mileageAdjustment;
|
||||
var averageGasMileage = (deltaMileage) / totalGallonsConsumed;
|
||||
if (!useMPG)
|
||||
{
|
||||
averageGasMileage = 100 / averageGasMileage;
|
||||
@@ -80,9 +81,16 @@ namespace CarCareTracker.Helper
|
||||
else if (currentObject.IsFillToFull)
|
||||
{
|
||||
//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
|
||||
unFactoredConsumption = 0;
|
||||
|
||||
@@ -14,11 +14,24 @@ namespace CarCareTracker.Helper
|
||||
if (existingReminder.Metric == ReminderMetric.Both)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -111,7 +111,8 @@ namespace CarCareTracker.Helper
|
||||
Cost = input.Cost,
|
||||
Mileage = input.Mileage,
|
||||
Files = input.Files,
|
||||
Notes = input.Notes
|
||||
Notes = input.Notes,
|
||||
Tags = input.Tags
|
||||
};
|
||||
}
|
||||
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
|
||||
@@ -124,7 +125,8 @@ namespace CarCareTracker.Helper
|
||||
Cost = input.Cost,
|
||||
Mileage = input.Mileage,
|
||||
Files = input.Files,
|
||||
Notes = input.Notes
|
||||
Notes = input.Notes,
|
||||
Tags = input.Tags
|
||||
};
|
||||
}
|
||||
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
|
||||
@@ -137,8 +139,32 @@ namespace CarCareTracker.Helper
|
||||
Cost = input.Cost,
|
||||
Mileage = input.Mileage,
|
||||
Files = input.Files,
|
||||
Notes = input.Notes
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
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 }; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
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 NoteText { 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 int Mileage { 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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
public int Mileage { get; set; }
|
||||
public string Notes { get; set; }
|
||||
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 Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public int CustomMileageInterval { get; set; } = 0;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public int CustomMileageInterval { get; set; } = 0;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||
@@ -22,6 +23,7 @@
|
||||
IsRecurring = IsRecurring,
|
||||
ReminderMileageInterval = ReminderMileageInterval,
|
||||
ReminderMonthInterval = ReminderMonthInterval,
|
||||
CustomMileageInterval = CustomMileageInterval,
|
||||
Notes = Notes }; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
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 ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||
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 ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
public TaxRecord ToTaxRecord() { return new TaxRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
@@ -20,6 +21,8 @@
|
||||
Notes = Notes,
|
||||
IsRecurring = IsRecurring,
|
||||
RecurringInterval = RecurringInterval,
|
||||
Files = Files }; }
|
||||
Files = Files,
|
||||
Tags = Tags
|
||||
}; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
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 }; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
public bool UseMarkDownOnSavedNotes { get; set; }
|
||||
public bool EnableAutoReminderRefresh { 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 UserPasswordHash { get; set;}
|
||||
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
public string LicensePlate { get; set; }
|
||||
public bool IsElectric { get; set; } = false;
|
||||
public bool UseHours { get; set; } = false;
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ A self-hosted, open-source vehicle service records and maintainence tracker.
|
||||
|
||||
Visit our website: https://lubelogger.com
|
||||
|
||||
Support this project on Patreon: https://patreon.com/LubeLogger
|
||||
Support this project by [Subscribing on Patreon](https://patreon.com/LubeLogger) or [Making a Donation](https://buy.stripe.com/aEU9Egc8DdMc9bO144)
|
||||
|
||||
## Why
|
||||
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
@inject IConfigHelper config
|
||||
@{
|
||||
var enableAuth = config.GetUserConfig(User).EnableAuth;
|
||||
var logoUrl = config.GetLogoUrl();
|
||||
}
|
||||
@model string
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger";
|
||||
}
|
||||
@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()">
|
||||
<ul class="navbar-nav" id="homeTab" role="tablist">
|
||||
<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 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>
|
||||
@@ -35,7 +36,7 @@
|
||||
<div class="container">
|
||||
<div class="row mt-2">
|
||||
<div class="d-flex lubelogger-navbar">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<img src="@logoUrl" />
|
||||
<div class="lubelogger-navbar-button">
|
||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
</div>
|
||||
@@ -44,7 +45,7 @@
|
||||
<hr />
|
||||
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(Model == "garage" ? "active" : "")" 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 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>
|
||||
@@ -69,9 +70,8 @@
|
||||
</ul>
|
||||
<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="row">
|
||||
<div id="garageContainer" class="row gy-3 align-items-stretch">
|
||||
</div>
|
||||
<div id="garageContainer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
@model List<Vehicle>
|
||||
|
||||
@if (Model.Any())
|
||||
@{
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@if (recordTags.Any())
|
||||
{
|
||||
foreach (Vehicle vehicle in Model)
|
||||
{
|
||||
<div class="col-xl-2 col-lg-4 col-md-4 col-sm-4 col-4 user-select-none" id="gridVehicle_@vehicle.Id" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" data-bs-trigger="manual" onmouseenter="loadPinnedNotes(@vehicle.Id)" ontouchstart="loadPinnedNotes(@vehicle.Id)" ontouchcancel="hidePinnedNotes(@vehicle.Id)" ontouchend="hidePinnedNotes(@vehicle.Id)" onmouseleave="hidePinnedNotes(@vehicle.Id)">
|
||||
<div class="card" onclick="viewVehicle(@vehicle.Id)">
|
||||
<img src="@vehicle.ImageLocation" style="height:145px; object-fit:scale-down;" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-truncate">@($"{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 class='row'>
|
||||
<div class="d-flex align-items-center flex-wrap mt-4">
|
||||
@foreach (string recordTag in recordTags)
|
||||
{
|
||||
<span onclick="filterGarage(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 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 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 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>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
</div>
|
||||
<hr />
|
||||
<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">
|
||||
<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>
|
||||
@@ -158,14 +160,14 @@
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<small class="text-body-secondary">Version 1.1.0</small>
|
||||
<small class="text-body-secondary">Version 1.1.1</small>
|
||||
</div>
|
||||
<p class="lead">
|
||||
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
|
||||
</p>
|
||||
<p class="lead">
|
||||
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>
|
||||
<div class="d-flex justify-content-center">
|
||||
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
|
||||
@@ -219,6 +221,8 @@
|
||||
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
|
||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
|
||||
preferredGasUnit: $("#preferredGasUnit").val(),
|
||||
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
|
||||
visibleTabs: visibleTabs,
|
||||
defaultTab: defaultTab
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
@{
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@{
|
||||
var logoUrl = config.GetLogoUrl();
|
||||
}
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Login";
|
||||
}
|
||||
@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="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">Username</label>
|
||||
<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";
|
||||
}
|
||||
@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="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">Username</label>
|
||||
<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";
|
||||
}
|
||||
@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="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">Token</label>
|
||||
<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";
|
||||
}
|
||||
@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="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<img src="@logoUrl" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">Token</label>
|
||||
<input type="text" id="inputToken" class="form-control">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
var useDarkMode = userConfig.UseDarkMode;
|
||||
var enableCsvImports = userConfig.EnableCsvImports;
|
||||
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
|
||||
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
||||
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
|
||||
shortDatePattern = shortDatePattern.ToLower();
|
||||
@@ -29,9 +30,10 @@
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap-datepicker/css/bootstrap-datepicker.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/loader.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap-tagsinput/bootstrap-tagsinput.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css"/>
|
||||
<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="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||
@@ -42,6 +44,7 @@
|
||||
<script src="~/js/shared.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-tagsinput/bootstrap-tagsinput.js"></script>
|
||||
<script src="~/sweetalert/sweetalert2.all.min.js"></script>
|
||||
<script src="~/js/loader.js"></script>
|
||||
<script>
|
||||
@@ -49,7 +52,9 @@
|
||||
return {
|
||||
useDarkMode : "@useDarkMode" == "True",
|
||||
enableCsvImport : "@enableCsvImports" == "True",
|
||||
useMarkDown: "@useMarkDown" == "True"
|
||||
useMarkDown: "@useMarkDown" == "True",
|
||||
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
|
||||
useThreeDecimals: "@useThreeDecimals" == "True"
|
||||
}
|
||||
}
|
||||
function getShortDatePattern() {
|
||||
@@ -61,7 +66,7 @@
|
||||
//remove thousands separator.
|
||||
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
|
||||
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
|
||||
var currencySymbol = "@numberFormat.CurrencySymbol";
|
||||
var currencySymbol = decodeHTMLEntities("@numberFormat.CurrencySymbol");
|
||||
if (input == "---") {
|
||||
input = "0";
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
}
|
||||
@model Vehicle
|
||||
@section Scripts {
|
||||
<script src="~/js/vehicle.js" asp-append-version="true"></script>
|
||||
<script src="~/js/servicerecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/gasrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/collisionrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/taxrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/note.js" asp-append-version="true"></script>
|
||||
<script src="~/js/reports.js" asp-append-version="true"></script>
|
||||
<script src="~/js/supplyrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/planrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/odometerrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/vehicle.js"></script>
|
||||
<script src="~/js/servicerecord.js"></script>
|
||||
<script src="~/js/gasrecord.js"></script>
|
||||
<script src="~/js/collisionrecord.js"></script>
|
||||
<script src="~/js/taxrecord.js"></script>
|
||||
<script src="~/js/reminderrecord.js"></script>
|
||||
<script src="~/js/upgraderecord.js"></script>
|
||||
<script src="~/js/note.js"></script>
|
||||
<script src="~/js/reports.js"></script>
|
||||
<script src="~/js/supplyrecord.js"></script>
|
||||
<script src="~/js/planrecord.js"></script>
|
||||
<script src="~/js/odometerrecord.js"></script>
|
||||
<script src="~/lib/chart-js/chart.umd.js"></script>
|
||||
<script src="~/lib/drawdown/drawdown.js"></script>
|
||||
}
|
||||
@@ -148,7 +148,7 @@
|
||||
</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-content" id="reminderRecordModalContent">
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
{
|
||||
@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 class="col-md-6 col-12">
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@model List<CollisionRecord>
|
||||
<div class="row">
|
||||
@@ -10,6 +11,16 @@
|
||||
<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-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>
|
||||
@if (enableCsvImports)
|
||||
@@ -54,7 +65,7 @@
|
||||
<tbody>
|
||||
@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">@collisionRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
var gasCostFormat = useThreeDecimals ? "C3" : "C2";
|
||||
var useKwh = Model.UseKwh;
|
||||
var useHours = Model.UseHours;
|
||||
string preferredFuelEconomyUnit = userConfig.PreferredGasMileageUnit;
|
||||
string preferredGasUnit = userConfig.PreferredGasUnit;
|
||||
string consumptionUnit;
|
||||
string fuelEconomyUnit;
|
||||
string distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km");
|
||||
@@ -42,9 +44,16 @@
|
||||
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
|
||||
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
|
||||
{
|
||||
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary" id="averageFuelMileageLabel">@($"Average Fuel Economy: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}")</span>
|
||||
if (useMPG)
|
||||
{
|
||||
<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 Cost: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
|
||||
@@ -80,8 +89,8 @@
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">Date Refueled</th>
|
||||
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
|
||||
<th scope="col" class="col-2" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Consumption(@(consumptionUnit))</th>
|
||||
<th scope="col" class="col-4" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Fuel Economy(@(fuelEconomyUnit))</th>
|
||||
<th scope="col" class="col-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" 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" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Cost</th>
|
||||
<th scope="col" class="col-1" onclick="toggleSort('gas-tab-pane', this)" style="cursor:pointer;">Unit Cost</th>
|
||||
</tr>
|
||||
@@ -92,10 +101,10 @@
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
|
||||
<td class="col-2">@gasRecord.Date</td>
|
||||
<td class="col-2">@gasRecord.Mileage</td>
|
||||
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
|
||||
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||
<td class="col-2" data-gas-type="consumption">@gasRecord.Gallons.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.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>
|
||||
}
|
||||
</tbody>
|
||||
@@ -110,4 +119,15 @@
|
||||
|
||||
</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,7 +16,7 @@
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||
{
|
||||
@:barGraphLabels.push("@gasCost.MonthName");
|
||||
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||
@:barGraphColors.push('@barGraphColors[index]');
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||
{
|
||||
@:barGraphLabels.push("@gasCost.MonthName");
|
||||
@:barGraphLabels.push(decodeHTMLEntities("@gasCost.MonthName"));
|
||||
@:barGraphData.push(globalParseFloat('@gasCost.Cost'));
|
||||
var index = sortedByMPG.FindIndex(x => x.MonthName == gasCost.MonthName);
|
||||
@:barGraphColors.push('@barGraphColors[index]');
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
<label for="noteTextArea">Notes<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
|
||||
<textarea class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea>
|
||||
</div>
|
||||
<div 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>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
@model List<Note>
|
||||
@{
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="ms-2 badge bg-success">@($"# 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>
|
||||
<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,7 +39,7 @@
|
||||
<tbody>
|
||||
@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)
|
||||
{
|
||||
<td class="col-3"><i class='bi bi-pin-fill me-2'></i>@note.Description</td>
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
</div>
|
||||
<label for="odometerRecordMileage">Odometer</label>
|
||||
<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 class="col-md-6 col-12">
|
||||
<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>
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
@{
|
||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@model List<OdometerRecord>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="ms-2 badge bg-success">@($"# 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>
|
||||
@if (enableCsvImports)
|
||||
@@ -51,7 +62,7 @@
|
||||
<tbody>
|
||||
@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-3">@odometerRecord.Mileage</td>
|
||||
<td class="col-7 col-xl-8 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes, 75)</td>
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
<label class="form-check-label" for="reminderIsRecurring">Is Recurring</label>
|
||||
</div>
|
||||
<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="OneHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.OneHundredMiles ? "selected" : "")>100 mi. / Km</!option>
|
||||
<!option value="FiveHundredMiles" @(Model.ReminderMileageInterval == ReminderMileageInterval.FiveHundredMiles ? "selected" : "")>500 mi. / Km</!option>
|
||||
@@ -95,7 +96,8 @@
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
var customMileageInterval = @Model.CustomMileageInterval;
|
||||
function getReminderRecordModelData() {
|
||||
return { id: @Model.Id}
|
||||
return { id: @Model.Id, mileageInterval: '@Model.ReminderMileageInterval.ToString()'}
|
||||
}
|
||||
</script>
|
||||
@@ -27,6 +27,13 @@
|
||||
{
|
||||
@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 class="col-md-6 col-12">
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@model List<ServiceRecord>
|
||||
<div class="row">
|
||||
@@ -10,6 +11,16 @@
|
||||
<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-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>
|
||||
@if (enableCsvImports)
|
||||
@@ -54,7 +65,7 @@
|
||||
<tbody>
|
||||
@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">@serviceRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@serviceRecord.Description</td>
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
<input type="text" id="taxRecordDescription" class="form-control" placeholder="Description of tax paid(i.e. Registration)" value="@Model.Description">
|
||||
<label for="taxRecordCost">Cost</label>
|
||||
<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 class="col-md-6 col-12">
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@model List<TaxRecord>
|
||||
<div class="row">
|
||||
@@ -10,6 +11,16 @@
|
||||
<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-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>
|
||||
@if (enableCsvImports)
|
||||
@@ -53,7 +64,7 @@
|
||||
<tbody>
|
||||
@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-4 col-xl-6">@taxRecord.Description</td>
|
||||
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
{
|
||||
@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 class="col-md-6 col-12">
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var recordTags = Model.SelectMany(x => x.Tags).Distinct();
|
||||
}
|
||||
@model List<UpgradeRecord>
|
||||
<div class="row">
|
||||
@@ -10,6 +11,16 @@
|
||||
<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-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>
|
||||
@if (enableCsvImports)
|
||||
@@ -54,7 +65,7 @@
|
||||
<tbody>
|
||||
@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">@upgradeRecord.Mileage</td>
|
||||
<td class="col-3 col-xl-4">@upgradeRecord.Description</td>
|
||||
|
||||
@@ -2,28 +2,6 @@
|
||||
@inject IConfigHelper config
|
||||
@{
|
||||
var hideZero = config.GetUserConfig(User).HideZero;
|
||||
var useMPG = config.GetUserConfig(User).UseMPG;
|
||||
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
|
||||
var useKwh = Model.VehicleData.IsElectric;
|
||||
var useHours = Model.VehicleData.UseHours;
|
||||
string fuelEconomyUnit;
|
||||
if (useKwh)
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
@model VehicleHistoryViewModel
|
||||
<div class="vehicleDetailTabContainer">
|
||||
@@ -58,7 +36,7 @@
|
||||
<div class="col-6">
|
||||
<ul class="list-group">
|
||||
<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 on Fuel: @Model.TotalGasCost.ToString("C")</li>
|
||||
</ul>
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
<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))
|
||||
{
|
||||
<label for="inputImage">Replace picture(optional)</label>
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"EnableAutoOdometerInsert": false,
|
||||
"UseUKMPG": false,
|
||||
"UseThreeDecimalGasCost": true,
|
||||
"UseMarkDownOnSavedNotes": false,
|
||||
"UseMarkDownOnSavedNotes": false,
|
||||
"PreferredGasMileageUnit": "",
|
||||
"PreferredGasUnit": "",
|
||||
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
|
||||
"DefaultTab": 8,
|
||||
"UserNameHash": "",
|
||||
|
||||
@@ -300,4 +300,7 @@ input[type="file"] {
|
||||
}
|
||||
[data-bs-theme=light] .taskCard {
|
||||
background-color: rgba(80,80,80,0.25);
|
||||
}
|
||||
.override-hide{
|
||||
display: none !important;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
$("#collisionRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#collisionRecordDate'));
|
||||
initTagSelector($("#collisionRecordTag"));
|
||||
$('#collisionRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -14,6 +15,7 @@ function showEditCollisionRecordModal(collisionRecordId) {
|
||||
$("#collisionRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#collisionRecordDate'));
|
||||
initTagSelector($("#collisionRecordTag"));
|
||||
$('#collisionRecordModal').modal('show');
|
||||
$('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -80,6 +82,7 @@ function getAndValidateCollisionRecordValues() {
|
||||
var collisionDescription = $("#collisionRecordDescription").val();
|
||||
var collisionCost = $("#collisionRecordCost").val();
|
||||
var collisionNotes = $("#collisionRecordNotes").val();
|
||||
var collisionTags = $("#collisionRecordTag").val();
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var collisionRecordId = getCollisionRecordModelData().id;
|
||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||
@@ -120,6 +123,7 @@ function getAndValidateCollisionRecordValues() {
|
||||
notes: collisionNotes,
|
||||
files: uploadedFiles,
|
||||
supplies: selectedSupplies,
|
||||
tags: collisionTags,
|
||||
addReminderRecord: addReminderRecord
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
$.get('/Vehicle/AddVehiclePartialView', function (data) {
|
||||
if (data) {
|
||||
$("#addVehicleModalContent").html(data);
|
||||
initTagSelector($("#inputTag"));
|
||||
$('#addVehicleModal').modal('show');
|
||||
}
|
||||
})
|
||||
$('#addVehicleModal').modal('show');
|
||||
}
|
||||
function hideAddVehicleModal() {
|
||||
$('#addVehicleModal').modal('hide');
|
||||
@@ -43,12 +44,127 @@ function loadPinnedNotes(vehicleId) {
|
||||
hoveredGrid.attr("data-bs-title", htmlString);
|
||||
new bootstrap.Tooltip(hoveredGrid);
|
||||
hoveredGrid.tooltip("show");
|
||||
} else {
|
||||
//disable the tooltip
|
||||
hoveredGrid.attr("data-bs-title", "");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
hoveredGrid.tooltip("show");
|
||||
if (hoveredGrid.attr("data-bs-title") != '') {
|
||||
hoveredGrid.tooltip("show");
|
||||
}
|
||||
}
|
||||
}
|
||||
function hidePinnedNotes(vehicleId) {
|
||||
$(`#gridVehicle_${vehicleId}`).tooltip("hide");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,4 +132,206 @@ function getAndValidateGasRecordValues() {
|
||||
missedFuelUp: gasIsMissed,
|
||||
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) {
|
||||
if (data) {
|
||||
$("#noteModalContent").html(data);
|
||||
initTagSelector($("#noteRecordTag"));
|
||||
$('#noteModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -10,6 +11,7 @@ function showEditNoteModal(noteId) {
|
||||
$.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) {
|
||||
if (data) {
|
||||
$("#noteModalContent").html(data);
|
||||
initTagSelector($("#noteRecordTag"));
|
||||
$('#noteModal').modal('show');
|
||||
$('#noteModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -73,6 +75,7 @@ function getAndValidateNoteValues() {
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var noteId = getNoteModelData().id;
|
||||
var noteIsPinned = $("#noteIsPinned").is(":checked");
|
||||
var noteTags = $("#noteRecordTag").val();
|
||||
//validation
|
||||
var hasError = false;
|
||||
if (noteDescription.trim() == '') { //eliminates whitespace.
|
||||
@@ -93,6 +96,7 @@ function getAndValidateNoteValues() {
|
||||
vehicleId: vehicleId,
|
||||
description: noteDescription,
|
||||
noteText: noteText,
|
||||
pinned: noteIsPinned
|
||||
pinned: noteIsPinned,
|
||||
tags: noteTags
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
$("#odometerRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#odometerRecordDate'));
|
||||
initTagSelector($("#odometerRecordTag"));
|
||||
$('#odometerRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -14,6 +15,7 @@ function showEditOdometerRecordModal(odometerRecordId) {
|
||||
$("#odometerRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#odometerRecordDate'));
|
||||
initTagSelector($("#odometerRecordTag"));
|
||||
$('#odometerRecordModal').modal('show');
|
||||
$('#odometerRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -78,6 +80,7 @@ function getAndValidateOdometerRecordValues() {
|
||||
var serviceDate = $("#odometerRecordDate").val();
|
||||
var serviceMileage = parseInt(globalParseFloat($("#odometerRecordMileage").val())).toString();
|
||||
var serviceNotes = $("#odometerRecordNotes").val();
|
||||
var serviceTags = $("#odometerRecordTag").val();
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var odometerRecordId = getOdometerRecordModelData().id;
|
||||
//validation
|
||||
@@ -101,6 +104,7 @@ function getAndValidateOdometerRecordValues() {
|
||||
date: serviceDate,
|
||||
mileage: serviceMileage,
|
||||
notes: serviceNotes,
|
||||
tags: serviceTags,
|
||||
files: uploadedFiles
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,35 @@
|
||||
function hideAddReminderRecordModal() {
|
||||
$('#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) {
|
||||
if (e != undefined) {
|
||||
event.stopPropagation();
|
||||
@@ -108,6 +137,7 @@ function getAndValidateReminderRecordValues() {
|
||||
var reminderIsRecurring = $("#reminderIsRecurring").is(":checked");
|
||||
var reminderRecurringMonth = $("#reminderRecurringMonth").val();
|
||||
var reminderRecurringMileage = $("#reminderRecurringMileage").val();
|
||||
var reminderCustomMileageInterval = customMileageInterval;
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var reminderId = getReminderRecordModelData().id;
|
||||
//validation
|
||||
@@ -154,6 +184,7 @@ function getAndValidateReminderRecordValues() {
|
||||
metric: reminderOption,
|
||||
isRecurring: reminderIsRecurring,
|
||||
reminderMileageInterval: reminderRecurringMileage,
|
||||
reminderMonthInterval: reminderRecurringMonth
|
||||
reminderMonthInterval: reminderRecurringMonth,
|
||||
customMileageInterval: customMileageInterval
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
$("#serviceRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#serviceRecordDate'));
|
||||
initTagSelector($("#serviceRecordTag"));
|
||||
$('#serviceRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -14,6 +15,7 @@ function showEditServiceRecordModal(serviceRecordId) {
|
||||
$("#serviceRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#serviceRecordDate'));
|
||||
initTagSelector($("#serviceRecordTag"));
|
||||
$('#serviceRecordModal').modal('show');
|
||||
$('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -80,6 +82,7 @@ function getAndValidateServiceRecordValues() {
|
||||
var serviceDescription = $("#serviceRecordDescription").val();
|
||||
var serviceCost = $("#serviceRecordCost").val();
|
||||
var serviceNotes = $("#serviceRecordNotes").val();
|
||||
var serviceTags = $("#serviceRecordTag").val();
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var serviceRecordId = getServiceRecordModelData().id;
|
||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||
@@ -120,6 +123,7 @@ function getAndValidateServiceRecordValues() {
|
||||
notes: serviceNotes,
|
||||
files: uploadedFiles,
|
||||
supplies: selectedSupplies,
|
||||
tags: serviceTags,
|
||||
addReminderRecord: addReminderRecord
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ function saveVehicle(isEdit) {
|
||||
var vehicleYear = $("#inputYear").val();
|
||||
var vehicleMake = $("#inputMake").val();
|
||||
var vehicleModel = $("#inputModel").val();
|
||||
var vehicleTags = $("#inputTag").val();
|
||||
var vehicleLicensePlate = $("#inputLicensePlate").val();
|
||||
var vehicleIsElectric = $("#inputIsElectric").is(":checked");
|
||||
var vehicleUseHours = $("#inputUseHours").is(":checked");
|
||||
@@ -76,6 +77,7 @@ function saveVehicle(isEdit) {
|
||||
model: vehicleModel,
|
||||
licensePlate: vehicleLicensePlate,
|
||||
isElectric: vehicleIsElectric,
|
||||
tags: vehicleTags,
|
||||
useHours: vehicleUseHours
|
||||
}, function (data) {
|
||||
if (data) {
|
||||
@@ -137,6 +139,15 @@ function initDatePicker(input, futureOnly) {
|
||||
});
|
||||
}
|
||||
}
|
||||
function initTagSelector(input, noDataList) {
|
||||
if (noDataList) {
|
||||
input.tagsinput({
|
||||
useDataList: false
|
||||
});
|
||||
} else {
|
||||
input.tagsinput();
|
||||
}
|
||||
}
|
||||
|
||||
function showMobileNav() {
|
||||
$(".lubelogger-mobile-nav").addClass("lubelogger-mobile-nav-show");
|
||||
@@ -178,6 +189,7 @@ function toggleSort(tabName, sender) {
|
||||
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)
|
||||
@@ -217,4 +229,35 @@ function sortTable(tabName, columnName, desc) {
|
||||
}
|
||||
});
|
||||
$(`#${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');
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
$("#taxRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#taxRecordDate'));
|
||||
initTagSelector($("#taxRecordTag"));
|
||||
$('#taxRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -14,6 +15,7 @@ function showEditTaxRecordModal(taxRecordId) {
|
||||
$("#taxRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#taxRecordDate'));
|
||||
initTagSelector($("#taxRecordTag"));
|
||||
$('#taxRecordModal').modal('show');
|
||||
$('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -91,6 +93,7 @@ function getAndValidateTaxRecordValues() {
|
||||
var taxRecordId = getTaxRecordModelData().id;
|
||||
var taxIsRecurring = $("#taxIsRecurring").is(":checked");
|
||||
var taxRecurringMonth = $("#taxRecurringMonth").val();
|
||||
var taxTags = $("#taxRecordTag").val();
|
||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||
//validation
|
||||
var hasError = false;
|
||||
@@ -122,6 +125,7 @@ function getAndValidateTaxRecordValues() {
|
||||
notes: taxNotes,
|
||||
isRecurring: taxIsRecurring,
|
||||
recurringInterval: taxRecurringMonth,
|
||||
tags: taxTags,
|
||||
files: uploadedFiles,
|
||||
addReminderRecord: addReminderRecord
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
$("#upgradeRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#upgradeRecordDate'));
|
||||
initTagSelector($("#upgradeRecordTag"));
|
||||
$('#upgradeRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -14,6 +15,7 @@ function showEditUpgradeRecordModal(upgradeRecordId) {
|
||||
$("#upgradeRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
initDatePicker($('#upgradeRecordDate'));
|
||||
initTagSelector($("#upgradeRecordTag"));
|
||||
$('#upgradeRecordModal').modal('show');
|
||||
$('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
|
||||
if (getGlobalConfig().useMarkDown) {
|
||||
@@ -80,6 +82,7 @@ function getAndValidateUpgradeRecordValues() {
|
||||
var upgradeDescription = $("#upgradeRecordDescription").val();
|
||||
var upgradeCost = $("#upgradeRecordCost").val();
|
||||
var upgradeNotes = $("#upgradeRecordNotes").val();
|
||||
var upgradeTags = $("#upgradeRecordTag").val();
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var upgradeRecordId = getUpgradeRecordModelData().id;
|
||||
var addReminderRecord = $("#addReminderCheck").is(":checked");
|
||||
@@ -120,6 +123,7 @@ function getAndValidateUpgradeRecordValues() {
|
||||
notes: upgradeNotes,
|
||||
files: uploadedFiles,
|
||||
supplies: selectedSupplies,
|
||||
tags: upgradeTags,
|
||||
addReminderRecord: addReminderRecord
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,7 @@ function editVehicle(vehicleId) {
|
||||
$.get(`/Vehicle/GetEditVehiclePartialViewById?vehicleId=${vehicleId}`, function (data) {
|
||||
if (data) {
|
||||
$("#editVehicleModalContent").html(data);
|
||||
initTagSelector($("#inputTag"), true);
|
||||
$('#editVehicleModal').modal('show');
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user