From 93065de77f80f9f1b40b2bc600d9fe03ea0bee92 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Wed, 21 Oct 2015 20:32:37 -0400 Subject: [PATCH] Service reminder implemented. --- Web/Controllers/ServiceReminderController.cs | 63 ++++++-- Web/Controllers/VehicleController.cs | 6 +- Web/Controllers/VehicleServiceController.cs | 105 ++++++++++++- Web/DAL/DataService.cs | 24 ++- Web/Models/ServiceReminder.cs | 3 +- Web/Scripts/Shared/FuelLogImport.js | 7 +- Web/Scripts/Shared/Site.js | 89 +++++++++-- .../ServiceReminderViewModel.cs | 49 +++++- .../Vehicle/VehicleResultsViewModel.cs | 4 +- Web/ViewModels/Vehicle/VehicleViewModel.cs | 142 ++++++++++++++++++ .../UpdateServiceRemindersViewModel.cs | 84 +++++++++++ .../VehicleServiceResultsViewModel.cs | 4 + .../VehicleService/VehicleServiceViewModel.cs | 2 + Web/Views/ServiceReminder/Create.cshtml | 5 +- Web/Views/ServiceReminder/Details.cshtml | 2 +- Web/Views/ServiceReminder/Edit.cshtml | 7 +- Web/Views/ServiceReminder/Index.cshtml | 8 +- .../ServiceReminderViewModel.cshtml | 2 +- Web/Views/Shared/BackToLogs.cshtml | 2 +- Web/Views/User/BackToUsers.cshtml | 2 +- Web/Views/Vehicle/BackToVehicles.cshtml | 2 +- Web/Views/Vehicle/Details.cshtml | 4 +- Web/Views/Vehicle/Index.cshtml | 42 +++--- Web/Views/VehicleService/Index.cshtml | 48 ++++-- .../UpdateServiceReminders.cshtml | 32 ++++ Web/Web.csproj | 3 + 26 files changed, 652 insertions(+), 89 deletions(-) create mode 100644 Web/ViewModels/Vehicle/VehicleViewModel.cs create mode 100644 Web/ViewModels/VehicleService/UpdateServiceRemindersViewModel.cs create mode 100644 Web/Views/VehicleService/UpdateServiceReminders.cshtml diff --git a/Web/Controllers/ServiceReminderController.cs b/Web/Controllers/ServiceReminderController.cs index 7abcad6..6a1fbb9 100644 --- a/Web/Controllers/ServiceReminderController.cs +++ b/Web/Controllers/ServiceReminderController.cs @@ -21,22 +21,6 @@ namespace MileageTraker.Web.Controllers return View(viewModel); } - [HttpGet] - [RequireRequestValue("vehicleId")] - public ActionResult Create(string vehicleId) - { - var viewModel = new ServiceReminderViewModel - { - VehicleId = vehicleId, - }; - - var vehicle = DataService.GetVehicle(vehicleId); - if (vehicle.CurrentOdometer.HasValue) - viewModel.TargetOdometer = (int) (Math.Ceiling((decimal) ((vehicle.CurrentOdometer.Value + 3000)/1000))*1000); - - return View(viewModel); - } - public ActionResult Delete(int id) { var serviceReminder = DataService.GetServiceReminder(id); @@ -45,6 +29,24 @@ namespace MileageTraker.Web.Controllers return RedirectToAction("Index", new { vehicleId }); } + [HttpGet] + [RequireRequestValue("vehicleId")] + public ActionResult Create(string vehicleId) + { + var vehicle = DataService.GetVehicle(vehicleId); + + var viewModel = new ServiceReminderViewModel + { + VehicleId = vehicleId, + CurrentOdometer = vehicle.CurrentOdometer + }; + + if (vehicle.CurrentOdometer.HasValue) + viewModel.TargetOdometer = vehicle.CurrentOdometer.Value + 4000; + + return View(viewModel); + } + [HttpPost] [ActionLog] public ActionResult Create(ServiceReminderViewModel viewModel) @@ -54,6 +56,7 @@ namespace MileageTraker.Web.Controllers var serviceReminder = viewModel.GetServiceReminder(); serviceReminder.Vehicle = DataService.GetVehicle(viewModel.VehicleId); + DataService.AddServiceReminder(serviceReminder); SetStatusMessage( @@ -63,5 +66,33 @@ namespace MileageTraker.Web.Controllers return View(viewModel); } + + [HttpGet] + public ActionResult Edit(int id) + { + var serviceReminder = DataService.GetServiceReminder(id); + var viewModel = new ServiceReminderViewModel(serviceReminder); + return View(viewModel); + } + + [HttpPost] + [ActionLog] + public ActionResult Edit(ServiceReminderViewModel viewModel) + { + if (ModelState.IsValid) + { + var serviceReminder = viewModel.GetServiceReminder(); + + serviceReminder.Vehicle = DataService.GetVehicle(viewModel.VehicleId); + + DataService.UpdateServiceReminder(serviceReminder); + + SetStatusMessage( + string.Format("Service Reminder at {0} miles updated", viewModel.TargetOdometer), StatusType.Success); + return RedirectToAction("Index", new {vehicleId = viewModel.VehicleId}); + } + + return View(viewModel); + } } } \ No newline at end of file diff --git a/Web/Controllers/VehicleController.cs b/Web/Controllers/VehicleController.cs index 9e2e061..1a270d8 100644 --- a/Web/Controllers/VehicleController.cs +++ b/Web/Controllers/VehicleController.cs @@ -13,7 +13,11 @@ namespace MileageTraker.Web.Controllers { public ViewResult Index(bool inactive = false) { - var vehicles = DataService.GetVehicles().Where(v => inactive == (v.InactiveDate.HasValue && v.InactiveDate.Value < DateTime.Today.AddDays(1))); + var vehicles = + (from v in DataService.GetVehicles().ToList() + where inactive == (v.InactiveDate.HasValue && v.InactiveDate.Value < DateTime.Today.AddDays(1)) + select new VehicleViewModel(v)).ToList(); + var viewModel = new VehicleResultsViewModel(vehicles, inactive); return View(viewModel); } diff --git a/Web/Controllers/VehicleServiceController.cs b/Web/Controllers/VehicleServiceController.cs index 783a8e5..41eb9fa 100644 --- a/Web/Controllers/VehicleServiceController.cs +++ b/Web/Controllers/VehicleServiceController.cs @@ -1,9 +1,12 @@ -using System; +using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using MileageTraker.Web.Attributes; using MileageTraker.Web.DAL; +using MileageTraker.Web.Models; using MileageTraker.Web.Utility; +using MileageTraker.Web.ViewModels; +using MileageTraker.Web.ViewModels.ServiceReminder; using MileageTraker.Web.ViewModels.VehicleService; namespace MileageTraker.Web.Controllers @@ -23,10 +26,16 @@ namespace MileageTraker.Web.Controllers var vehicleServices = DataService.FilterVehicleServices(DataService.GetVehicleServices(), query).ToList(); + var upcomingServiceReminders = + DataService.GetUpcomingServiceReminders().ToList() + .Select(sr => new ServiceReminderViewModel(sr)).ToList(); + upcomingServiceReminders.Sort(); + var viewModel = new VehicleServiceResultsViewModel( - vehicleServices.Select(s => new VehicleServiceViewModel(s)), - query, - CustomExtensions.YearMonthList(validVehicleServiceYearMonths)); + vehicleServices.Select(s => new VehicleServiceViewModel(s)), + upcomingServiceReminders, + query, + CustomExtensions.YearMonthList(validVehicleServiceYearMonths)); return View(viewModel); } @@ -78,18 +87,104 @@ namespace MileageTraker.Web.Controllers { var vehicleService = viewModel.GetVehicleService(); - vehicleService.Vehicle = DataService.GetVehicle(viewModel.VehicleId); DataService.AddVehicleService(vehicleService); SetStatusMessage( string.Format("Vehicle Service for vehicle {0} created", viewModel.VehicleId), StatusType.Success); + return RedirectToAction("UpdateServiceReminders", new {vehicleId = viewModel.VehicleId}); + } + + return View(viewModel); + } + + [HttpGet] + public ActionResult UpdateServiceReminders(string vehicleId) + { + var vehicle = DataService.GetVehicle(vehicleId); + + var viewModel = new UpdateServiceRemindersViewModel { VehicleId = vehicleId }; + + RefreshServiceReminderViewModel(viewModel, vehicle, true); + + if (vehicle.CurrentOdometer.HasValue) + viewModel.TargetOdometer = vehicle.CurrentOdometer.Value + 4000; + + return View(viewModel); + } + + [HttpPost] + [ActionLog] + public ActionResult UpdateServiceReminders(UpdateServiceRemindersViewModel viewModel) + { + var vehicle = DataService.GetVehicle(viewModel.VehicleId); + + RefreshServiceReminderViewModel(viewModel, vehicle); // this data doesn't get posted back + + if (ModelState.IsValid) + { + var serviceReminder = viewModel.GetServiceReminder(); + + var status = new List(); + + // handle the new service reminder + if (serviceReminder != null) + { + serviceReminder.Vehicle = vehicle; + + DataService.AddServiceReminder(serviceReminder); + status.Add(string.Format("Service Reminder at {0} miles created", viewModel.TargetOdometer)); + } + + // handle any deletion + if (viewModel.DeleteServiceReminders != null + && viewModel.DeleteServiceReminders.Selected != null + && viewModel.DeleteServiceReminders.Selected.Length > 0) + { + var serviceReminders = + (from sr in vehicle.ServiceReminders + select new { format = UpdateServiceRemindersViewModel.FormatServiceReminder(sr), id = sr.ServiceReminderId }) + .ToDictionary(o => o.format); + + foreach (var selected in viewModel.DeleteServiceReminders.Selected) + { + DataService.DeleteServiceReminder(serviceReminders[selected].id); + } + status.Add(string.Format("Selected {0} service reminders deleted", viewModel.DeleteServiceReminders.Selected.Count())); + } + + if (status.Count > 0) + { + SetStatusMessage(string.Join(", ", status), StatusType.Success); + } return RedirectToAction("Index"); } return View(viewModel); } + private void RefreshServiceReminderViewModel( + UpdateServiceRemindersViewModel viewModel, Vehicle vehicle, bool setDefaultSelectedServiceReminders = false) + { + if (viewModel.DeleteServiceReminders == null) + viewModel.DeleteServiceReminders = new CheckBoxViewModel(); + + var existingServiceReminders = + (from sr in vehicle.ServiceReminders + select UpdateServiceRemindersViewModel.FormatServiceReminder(sr)).ToArray(); + + viewModel.DeleteServiceReminders.Available = existingServiceReminders; + viewModel.CurrentOdometer = vehicle.CurrentOdometer; + + if (vehicle.CurrentOdometer.HasValue && setDefaultSelectedServiceReminders) + { + viewModel.DeleteServiceReminders.Selected = + (from sr in vehicle.ServiceReminders + where sr.TargetOdometer <= vehicle.CurrentOdometer.Value + select UpdateServiceRemindersViewModel.FormatServiceReminder(sr)).ToArray(); + } + } + public ActionResult Edit(int id) { var vehicleService = DataService.GetVehicleService(id); diff --git a/Web/DAL/DataService.cs b/Web/DAL/DataService.cs index 53a851a..cde89c1 100644 --- a/Web/DAL/DataService.cs +++ b/Web/DAL/DataService.cs @@ -407,7 +407,7 @@ namespace MileageTraker.Web.DAL _db.SaveChanges(); } - public IEnumerable GetVehicles() + public IQueryable GetVehicles() { var vehicles = _db.Vehicles; return vehicles; @@ -901,6 +901,17 @@ namespace MileageTraker.Web.DAL return _db.ServiceReminders.Find(id); } + public ServiceReminder FindDuplicateServiceReminder(string vehicleId, int targetOdometer, string description) + { + return + (from sr in _db.ServiceReminders + where + sr.Vehicle.VehicleId == vehicleId && + sr.TargetOdometer == targetOdometer && + sr.Description == description + select sr).FirstOrDefault(); + } + public void DeleteServiceReminder(int id) { var serviceReminder = _db.ServiceReminders.Find(id); @@ -909,6 +920,17 @@ namespace MileageTraker.Web.DAL _db.SaveChanges(); } + public IQueryable GetUpcomingServiceReminders() + { + const int mileageThreshold = 500; + return + from sr in _db.ServiceReminders + where + sr.Vehicle.CurrentOdometer.HasValue + && sr.Vehicle.CurrentOdometer.Value > (sr.TargetOdometer - mileageThreshold) + select sr; + } + #endregion } } \ No newline at end of file diff --git a/Web/Models/ServiceReminder.cs b/Web/Models/ServiceReminder.cs index a0a17eb..cf7bea6 100644 --- a/Web/Models/ServiceReminder.cs +++ b/Web/Models/ServiceReminder.cs @@ -12,7 +12,8 @@ namespace MileageTraker.Web.Models public virtual Vehicle Vehicle { get; set; } [InputSize("small")] - public int TargetOdometer { get; set; } + [Range(1, 500000, ErrorMessage = "Between 1 and 500k")] + public int TargetOdometer { get; set; } [StringLength(64)] public string Description { get; set; } diff --git a/Web/Scripts/Shared/FuelLogImport.js b/Web/Scripts/Shared/FuelLogImport.js index e35ce0a..42d9ff7 100644 --- a/Web/Scripts/Shared/FuelLogImport.js +++ b/Web/Scripts/Shared/FuelLogImport.js @@ -5,6 +5,8 @@ $('#page-match-status').html(' Matching In Progress for ' + total + ' fuel logs. Keep page open until complete.'); $('#page-match-status').after('
'); + $(".match-status a").addClass('pull-right'); + submitNext(); var unmatchedCount = 0; var errorCount = 0; @@ -41,10 +43,11 @@ } $('.match-message', $row).text(result.Message); } + if (result.Action != undefined && result.Action != null) { $('.match-status', $row).append(" " + result.Action); - var $action = $("a[matchcount]", $row); - matchCountFunc.apply($action); + $("a", $row).addClass('pull-right'); + matchCountFunc.apply($("a[matchcount]", $row)); } submitNext(); diff --git a/Web/Scripts/Shared/Site.js b/Web/Scripts/Shared/Site.js index 8c975fc..f7814a7 100644 --- a/Web/Scripts/Shared/Site.js +++ b/Web/Scripts/Shared/Site.js @@ -75,7 +75,7 @@ $(function () { var $recentLogs = $("#RecentLogs"); if ($recentLogs.length > 0) { $.ajax({ - url: "/FuelLog/RecentLogs", + url: "/CreateLog/RecentLogs", success: function (data) { $recentLogs.append(data); } @@ -83,12 +83,69 @@ $(function () { } }); +/** +Ability to run the defered promises in sequence +*/ +(function ($) { + "use strict"; + var copy = function (a) { + return Array.prototype.slice.call(a); + }; + + /** + Handle a sequence of methods, stopping on failure by default + @param Array chain List of methods to execute. Non-deferred return values will be treated as successful deferreds. + @param Boolean continueOnFailure Continue executing even if one of the returned deferreds fails. + @returns Deferred + */ + $.sequence = function (chain, continueOnFailure) { + var handleStep, handleResult, + steps = copy(chain), + def = new $.Deferred(), + defs = [], + results = []; + handleStep = function () { + if (!steps.length) { + def.resolveWith(defs, [results]); + return; + } + var step = steps.shift(), + result = step; // used to be step();, but we're already dealing with promises + handleResult( + $.when(result).always(function () { + defs.push(this); + }).done(function () { + results.push({ resolved: copy(arguments) }); + }).fail(function () { + results.push({ rejected: copy(arguments) }); + }) + ); + }; + handleResult = continueOnFailure ? + function (result) { + result.always(function () { + handleStep(); + }); + } : + function (result) { + result.done(handleStep) + .fail(function () { + def.rejectWith(defs, [results]); + }); + }; + handleStep(); + return def.promise(); + }; +}(this.jQuery)); + var matchCountFunc = function() { var $link = $(this); var url = $link.attr("matchcount"); + $link.append(' '); return $.ajax({ url: url, - success: function(matchcount) { + success: function (matchcount) { + $('span', $link).remove(); if (matchcount > 0) { $link.append(" " + matchcount + ""); } else { @@ -101,7 +158,7 @@ var matchCountFunc = function() { // add get match count for all the current items $(function () { var $requests = $("a[matchcount]").map(matchCountFunc); - $.when.apply($, $requests); + $.sequence($requests); }); $(function() { @@ -124,7 +181,7 @@ $(function() { var idNavActiveRegex = { 'fuellog-nav': /\/fuellog/i, 'log-nav': /\/log/i, - 'vehicle-nav': /\/vehicle/i, + 'vehicle-nav': /\/vehicle|\/servicereminder/i, 'user-nav': /\/user/i, 'config-nav': /\/City|\/Purpose/i }; @@ -142,23 +199,33 @@ $(function() { function addButtonIcons () { var textToIcon = { - 'Edit': 'edit', + 'Enter Log': 'plus', + 'Edit': 'edit', 'Filter': 'filter', - 'Details' : 'zoom-in', + 'Details': 'info-circle', 'Delete': 'trash', 'Add': 'plus', 'Export': 'download', 'Import': 'upload', 'Driver Mileage': 'user', 'Vehicle Mileage': 'car', - 'Show Active': 'ok-circle', - 'Show Inactive': 'ban-circle', - 'Set Inactive' : 'ban-circle', - 'Reactivate' : 'ok-circle' + 'Show Active': 'check-circle', + 'Show Inactive': 'ban', + 'Set Inactive' : 'ban', + 'Reactivate': 'check-circle', + 'Config': 'cog', + 'Cities': 'map', + 'Purposes': 'arrow-right', + 'Vehicle Service': 'wrench', + 'Vehicle': 'car', + 'Reminder': 'clock-o', + 'Fuel Logs': 'tachometer', + 'Users': 'user', + 'Logs': 'road' }; $.each(textToIcon, function(text, icon) { $("a:contains('" + text + "'):not(:has(i))") - .prepend(' '); + .prepend(' '); }); $(".navbar-inverse a[title='Manage']:not(:has(i))") diff --git a/Web/ViewModels/ServiceReminder/ServiceReminderViewModel.cs b/Web/ViewModels/ServiceReminder/ServiceReminderViewModel.cs index ef8f745..2b9c1e2 100644 --- a/Web/ViewModels/ServiceReminder/ServiceReminderViewModel.cs +++ b/Web/ViewModels/ServiceReminder/ServiceReminderViewModel.cs @@ -1,11 +1,14 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using AutoMapper; using MileageTraker.Web.Attributes; +using MileageTraker.Web.DAL; namespace MileageTraker.Web.ViewModels.ServiceReminder { - public class ServiceReminderViewModel + public class ServiceReminderViewModel : IComparable, IValidatableObject { [HiddenInput(DisplayValue = false)] public int? ServiceReminderId { get; set; } @@ -19,17 +22,28 @@ namespace MileageTraker.Web.ViewModels.ServiceReminder [InputSize("mini")] public string VehicleId { get; set; } + [HiddenInput(DisplayValue = false)] + public int? CurrentOdometer { get; set; } + [InputSize("small")] - public int TargetOdometer { get; set; } + [Range(1, 500000, ErrorMessage = "Between 1 and 500k")] + public int TargetOdometer { get; set; } [StringLength(64)] public string Description { get; set; } + [HiddenInput(DisplayValue = false)] + public bool IsServiceOverdue { get { return ServiceDueInMiles <= 0; } } + + [HiddenInput(DisplayValue = false)] + public int ServiceDueInMiles { get { return CurrentOdometer.HasValue ? TargetOdometer - CurrentOdometer.Value : int.MaxValue; } } + static ServiceReminderViewModel() { Mapper.CreateMap(); Mapper.CreateMap() - .ForMember(dest => dest.VehicleId, opt => opt.MapFrom(src => src.Vehicle.VehicleId)); + .ForMember(dest => dest.VehicleId, opt => opt.MapFrom(src => src.Vehicle.VehicleId)) + .ForMember(dest => dest.CurrentOdometer, opt => opt.MapFrom(src => src.Vehicle.CurrentOdometer)); } public ServiceReminderViewModel(Models.ServiceReminder serviceReminder) @@ -47,5 +61,32 @@ namespace MileageTraker.Web.ViewModels.ServiceReminder Mapper.Map(this, serviceReminder); return serviceReminder; } + + public int CompareTo(ServiceReminderViewModel other) + { + return ServiceDueInMiles - other.ServiceDueInMiles; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + using (var dataService = new DataService()) + { + var duplicateServiceReminder = dataService.FindDuplicateServiceReminder(VehicleId, TargetOdometer, Description); + + if (duplicateServiceReminder != null) + yield return new ValidationResult("Exact duplicate of this reminder already exists"); + + var vehicle = dataService.GetVehicle(VehicleId); + + if (vehicle.InactiveDate.HasValue) + yield return new ValidationResult("Vehicle is inactive, no service reminder permitted"); + + // no reminders for mileage less than the current odometer + if (vehicle.CurrentOdometer.HasValue && TargetOdometer <= vehicle.CurrentOdometer) + yield return new ValidationResult( + string.Format("Target odometer {0} must be greater than current odometer {1}", + TargetOdometer, vehicle.CurrentOdometer.Value), new[]{"TargetOdometer"}); + } + } } } \ No newline at end of file diff --git a/Web/ViewModels/Vehicle/VehicleResultsViewModel.cs b/Web/ViewModels/Vehicle/VehicleResultsViewModel.cs index 9c355fb..192ebca 100644 --- a/Web/ViewModels/Vehicle/VehicleResultsViewModel.cs +++ b/Web/ViewModels/Vehicle/VehicleResultsViewModel.cs @@ -4,10 +4,10 @@ namespace MileageTraker.Web.ViewModels.Vehicle { public class VehicleResultsViewModel { - public IEnumerable Vehicles { get; set; } + public IEnumerable Vehicles { get; set; } public bool Inactive { get; set; } - public VehicleResultsViewModel(IEnumerable vehicles, bool activeInactive) + public VehicleResultsViewModel(IEnumerable vehicles, bool activeInactive) { Vehicles = vehicles; Inactive = activeInactive; diff --git a/Web/ViewModels/Vehicle/VehicleViewModel.cs b/Web/ViewModels/Vehicle/VehicleViewModel.cs new file mode 100644 index 0000000..45f6702 --- /dev/null +++ b/Web/ViewModels/Vehicle/VehicleViewModel.cs @@ -0,0 +1,142 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Web.Mvc; +using AutoMapper; +using MileageTraker.Web.Attributes; + +namespace MileageTraker.Web.ViewModels.Vehicle +{ + public class VehicleViewModel + { + [Required] + [InputSize("mini")] + public int Key { get; set; } + + [Key] + [Required] + [StringLength(6, MinimumLength = 4, ErrorMessage = "Must be at least a 4 digit number")] + [Display(Name = "EHTRA ID")] + [RegularExpression(@"\d+", ErrorMessage = "Vehicle ID must be all numbers")] + [InputSize("mini")] + public string VehicleId { get; set; } + + [Required] + [RegularExpression(@"\d{4}", ErrorMessage = "Must be 4 numbers")] + [InputSize("mini")] + public string ModelYear { get; set; } + + [Required] + [InputSize("medium")] + public string Make { get; set; } + + [Display(Name = "Model")] + [Required] + [InputSize("medium")] + public string CarModel { get; set; } + + [InputSize("small")] + public string Color { get; set; } + + [Required] + [RegularExpression(@"Car|Truck|SUV|Van", ErrorMessage = "Must be Car, Truck, SUV, or Van")] + [FormatHint("Car, Truck, SUV or Van")] + [InputSize("mini")] + public string Type { get; set; } + + [Required] + [Display(Name = "VIN")] + [RegularExpression(@"[0-9A-HJ-NPR-Z]{17}", ErrorMessage = "VIN must be 17-characters, not including letters I, O or Q")] + [InputSize("large")] + public string Vin { get; set; } + + [Required] + [InputSize("small")] + [Currency] + public decimal Price { get; set; } + + [Required] + [RegularExpression(@"\d{1,2}/\d{2}", ErrorMessage = "PurDate must be in mm/yy format")] + [InputSize("small")] + [FormatHint("mm/yy")] + public string PurDate { get; set; } + + [DataType(DataType.DateTime)] + [DisplayFormat(NullDisplayText = "Currently Active", DataFormatString = @"{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)] + [InputSize("small")] + [FormatHint("mm/dd/yyyy")] + public DateTime? InactiveDate { get; set; } + + [Required] + [Display(Name = "Tag#")] + [InputSize("small")] + public string TagNumber { get; set; } + + [Required] + [InputSize("medium")] + public string Prog { get; set; } + + [RegularExpression(@"Unassigned|[A-Za-z().]+(\s+[A-Za-z().]+)+", ErrorMessage = "Please enter the full name")] + [DisplayFormat(NullDisplayText = "Unassigned")] + [FormatHint("Blank for Unassigned")] + public string Assigned { get; set; } + + [InputSize("medium")] + public string Notes { get; set; } + + [Display(Name = "Current Odometer", ShortName= "ODO")] + [DisplayFormat(NullDisplayText = "?")] + [InputSize("small")] + public int? CurrentOdometer { get; set; } + + [HiddenInput(DisplayValue = true)] + public int? NextServiceOdometer { get; set; } + + [HiddenInput(DisplayValue = false)] + public bool IsNextServiceOverdue + { + get { return NextServiceDueInMiles <= 0; } + } + + [HiddenInput(DisplayValue = false)] + public int NextServiceDueInMiles + { + get + { + return CurrentOdometer.HasValue && NextServiceOdometer.HasValue + ? NextServiceOdometer.Value - CurrentOdometer.Value + : int.MaxValue; + } + } + + + static VehicleViewModel() + { + Mapper.CreateMap(); + Mapper.CreateMap() + .ForMember(dest => dest.NextServiceOdometer, + opt => opt.ResolveUsing(v => + v.ServiceReminders + .OrderBy(s => s.TargetOdometer) + .Select(s => (int?) s.TargetOdometer) + .FirstOrDefault() + )); + } + + public VehicleViewModel(Models.Vehicle vehicle) + { + Mapper.Map(vehicle, this); + } + + public VehicleViewModel() + { + } + + public Models.Vehicle GetVehicle() + { + var vehicle = new Models.Vehicle(); + Mapper.Map(this, vehicle); + return vehicle; + } + } +} \ No newline at end of file diff --git a/Web/ViewModels/VehicleService/UpdateServiceRemindersViewModel.cs b/Web/ViewModels/VehicleService/UpdateServiceRemindersViewModel.cs new file mode 100644 index 0000000..001bbb6 --- /dev/null +++ b/Web/ViewModels/VehicleService/UpdateServiceRemindersViewModel.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; +using AutoMapper; +using MileageTraker.Web.Attributes; +using MileageTraker.Web.DAL; +using MileageTraker.Web.ViewModels.ServiceReminder; + +namespace MileageTraker.Web.ViewModels.VehicleService +{ + public class UpdateServiceRemindersViewModel : IValidatableObject + { + [Display(Name = "Existing Reminders")] + public CheckBoxViewModel DeleteServiceReminders { get; set; } + + [HiddenInput(DisplayValue = false)] + public int? ServiceReminderId { get; set; } + + [HiddenInput(DisplayValue = false)] + [Required] + [Remote("Exists", "Vehicle", ErrorMessage = "ID not found")] + [StringLength(6, MinimumLength = 4, ErrorMessage = "Must be at least a 4 digit number")] + [Display(Name = "Vehicle ID")] + [RegularExpression(@"\d+", ErrorMessage = "Vehicle ID must be all numbers")] + [InputSize("mini")] + public string VehicleId { get; set; } + + [HiddenInput(DisplayValue = false)] + public int? CurrentOdometer { get; set; } + + [InputSize("small")] + [Range(1, 500000, ErrorMessage = "Between 1 and 500k")] + public int? TargetOdometer { get; set; } + + [StringLength(64)] + public string Description { get; set; } + + static UpdateServiceRemindersViewModel() + { + Mapper.CreateMap(); + } + + public Models.ServiceReminder GetServiceReminder() + { + if (!TargetOdometer.HasValue || TargetOdometer.Value == 0) + return null; + var serviceReminder = new Models.ServiceReminder(); + Mapper.Map(this, serviceReminder); + return serviceReminder; + } + + public static string FormatServiceReminder(Models.ServiceReminder serviceReminder) + { + var s = "Target ODO: " + serviceReminder.TargetOdometer; + if (!string.IsNullOrEmpty(serviceReminder.Description)) + s += " Description: " + serviceReminder.Description; + return s; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (!TargetOdometer.HasValue) yield break; + + using (var dataService = new DataService()) + { + var duplicateServiceReminder = dataService.FindDuplicateServiceReminder(VehicleId, TargetOdometer.Value, Description); + + if (duplicateServiceReminder != null) + yield return new ValidationResult("Exact duplicate of this reminder already exists"); + + var vehicle = dataService.GetVehicle(VehicleId); + + if (vehicle.InactiveDate.HasValue) + yield return new ValidationResult("Vehicle is inactive, no service reminder permitted"); + + // no reminders for mileage less than the current odometer + if (vehicle.CurrentOdometer.HasValue && TargetOdometer <= vehicle.CurrentOdometer) + yield return new ValidationResult( + string.Format("Target odometer {0} must be greater than current odometer {1}", + TargetOdometer, vehicle.CurrentOdometer.Value), new[] {"TargetOdometer"}); + } + } + } +} \ No newline at end of file diff --git a/Web/ViewModels/VehicleService/VehicleServiceResultsViewModel.cs b/Web/ViewModels/VehicleService/VehicleServiceResultsViewModel.cs index 29d59ab..a5dc36d 100644 --- a/Web/ViewModels/VehicleService/VehicleServiceResultsViewModel.cs +++ b/Web/ViewModels/VehicleService/VehicleServiceResultsViewModel.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Globalization; +using MileageTraker.Web.ViewModels.ServiceReminder; namespace MileageTraker.Web.ViewModels.VehicleService { public class VehicleServiceResultsViewModel { public IEnumerable ServiceItems { get; set; } + public IList UpcomingServiceReminders { get; set; } public Dictionary> AvailableYearMonths { get; set; } public IEnumerable SelectedYearMonths { @@ -24,10 +26,12 @@ namespace MileageTraker.Web.ViewModels.VehicleService public VehicleServiceResultsViewModel( IEnumerable serviceItems, + IList upcomingServiceReminders, VehicleServiceQueryViewModel query, Dictionary> availableYearMonths) { ServiceItems = serviceItems; + UpcomingServiceReminders = upcomingServiceReminders; AvailableYearMonths = availableYearMonths; Year = query.Year.HasValue ? query.Year.Value.ToString(CultureInfo.InvariantCulture) : string.Empty; Month = query.Month.HasValue ? query.Month.Value.ToString(CultureInfo.InvariantCulture) : string.Empty; diff --git a/Web/ViewModels/VehicleService/VehicleServiceViewModel.cs b/Web/ViewModels/VehicleService/VehicleServiceViewModel.cs index 9ab3391..6db35e0 100644 --- a/Web/ViewModels/VehicleService/VehicleServiceViewModel.cs +++ b/Web/ViewModels/VehicleService/VehicleServiceViewModel.cs @@ -21,8 +21,10 @@ namespace MileageTraker.Web.ViewModels.VehicleService [DataType(DataType.DateTime)] [DisplayFormatAttribute(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")] + [DenyFutureDate(ErrorMessage = "Future date")] [FormatHint("mm/dd/yyyy")] [InputSize("small")] + [Required] public DateTime? InvoiceDate { get; set; } [Required] diff --git a/Web/Views/ServiceReminder/Create.cshtml b/Web/Views/ServiceReminder/Create.cshtml index 8f48f77..97a23b9 100644 --- a/Web/Views/ServiceReminder/Create.cshtml +++ b/Web/Views/ServiceReminder/Create.cshtml @@ -1,9 +1,11 @@ @model MileageTraker.Web.ViewModels.ServiceReminder.ServiceReminderViewModel @{ - ViewBag.Title = "Create Vehicle Service Reminder"; + ViewBag.Title = "Create Service Reminder"; } +@Html.Partial("_StatusMessage") +

@ViewBag.Title

@using (Html.BeginForm("Create", "ServiceReminder", FormMethod.Post, new { @class = "form-horizontal well center-content" })) @@ -11,6 +13,7 @@ @Html.Partial("_ValidationSummary")
+ @Html.DisplayFor(m => m.CurrentOdometer) @Html.EditorForModel()
diff --git a/Web/Views/ServiceReminder/Details.cshtml b/Web/Views/ServiceReminder/Details.cshtml index e8224bf..fa77e42 100644 --- a/Web/Views/ServiceReminder/Details.cshtml +++ b/Web/Views/ServiceReminder/Details.cshtml @@ -1,7 +1,7 @@ @model MileageTraker.Web.ViewModels.ServiceReminder.ServiceReminderViewModel @{ - ViewBag.Title = "Vehicle Service Reminder Details" ; + ViewBag.Title = "Service Reminder Details" ; } @Html.Partial("_StatusMessage") diff --git a/Web/Views/ServiceReminder/Edit.cshtml b/Web/Views/ServiceReminder/Edit.cshtml index ab36555..40abc50 100644 --- a/Web/Views/ServiceReminder/Edit.cshtml +++ b/Web/Views/ServiceReminder/Edit.cshtml @@ -1,19 +1,20 @@ @model MileageTraker.Web.ViewModels.ServiceReminder.ServiceReminderViewModel @{ - ViewBag.Title = "Edit Vehicle Service Reminder"; + ViewBag.Title = "Edit Service Reminder"; } @Html.Partial("_StatusMessage")

@ViewBag.Title

-@using (Html.BeginForm("Edit", "ServiceREminder", FormMethod.Post, new { @class = "form-horizontal well center-content" })) +@using (Html.BeginForm("Edit", "ServiceReminder", FormMethod.Post, new { @class = "form-horizontal well center-content" })) { @Html.Partial("_ValidationSummary")
- @Html.EditorForModel() + @Html.DisplayFor(m => m.CurrentOdometer) + @Html.EditorForModel()
diff --git a/Web/Views/ServiceReminder/Index.cshtml b/Web/Views/ServiceReminder/Index.cshtml index d9d9910..9a34a9a 100644 --- a/Web/Views/ServiceReminder/Index.cshtml +++ b/Web/Views/ServiceReminder/Index.cshtml @@ -1,12 +1,13 @@ @model MileageTraker.Web.ViewModels.ServiceReminder.ServiceReminderResultsViewModel @{ - ViewBag.Title = "Vehicle Service Reminders for Vehicle Id " + Model.VehicleId; + ViewBag.Title = "Service Reminders"; } @Html.Partial("_StatusMessage") -

@ViewBag.Title

+

@ViewBag.Title

+

for Vehicle @Model.VehicleId

@foreach (var item in Model.ServiceReminderItems) @@ -16,6 +17,7 @@
- @Html.ActionLink("Add Service Reminder", "Create", new { vehicleId = Model.VehicleId }, new{@class="btn"}) + @Html.ActionLink("Add Reminder", "Create", new { vehicleId = Model.VehicleId }, new{@class="btn"}) + @Html.ActionLink("Add Service", "Create", "VehicleService", new { vehicleId = Model.VehicleId }, new { @class = "btn" }) @Html.ActionLink("Vehicle Details", "Details", "Vehicle", new { id = Model.VehicleId }, new{@class="btn"})
\ No newline at end of file diff --git a/Web/Views/ServiceReminder/ServiceReminderViewModel.cshtml b/Web/Views/ServiceReminder/ServiceReminderViewModel.cshtml index c7b06a0..9eef63f 100644 --- a/Web/Views/ServiceReminder/ServiceReminderViewModel.cshtml +++ b/Web/Views/ServiceReminder/ServiceReminderViewModel.cshtml @@ -1,7 +1,7 @@ @model MileageTraker.Web.ViewModels.ServiceReminder.ServiceReminderViewModel
-

Service Reminder

+

Service Reminder

@Html.DisplayForModel()
@Html.ActionLink("Edit", "Edit", new { id = Model.ServiceReminderId }, new { @class = "btn" }) diff --git a/Web/Views/Shared/BackToLogs.cshtml b/Web/Views/Shared/BackToLogs.cshtml index 66f9d5a..e63c14e 100644 --- a/Web/Views/Shared/BackToLogs.cshtml +++ b/Web/Views/Shared/BackToLogs.cshtml @@ -1,5 +1,5 @@ @if (Session["LogPage"] != null) { } \ No newline at end of file diff --git a/Web/Views/User/BackToUsers.cshtml b/Web/Views/User/BackToUsers.cshtml index 2813354..67c7259 100644 --- a/Web/Views/User/BackToUsers.cshtml +++ b/Web/Views/User/BackToUsers.cshtml @@ -1,5 +1,5 @@  \ No newline at end of file diff --git a/Web/Views/Vehicle/BackToVehicles.cshtml b/Web/Views/Vehicle/BackToVehicles.cshtml index 2bc8665..2abf542 100644 --- a/Web/Views/Vehicle/BackToVehicles.cshtml +++ b/Web/Views/Vehicle/BackToVehicles.cshtml @@ -1,5 +1,5 @@  diff --git a/Web/Views/Vehicle/Details.cshtml b/Web/Views/Vehicle/Details.cshtml index 0c7a09f..01814b9 100644 --- a/Web/Views/Vehicle/Details.cshtml +++ b/Web/Views/Vehicle/Details.cshtml @@ -34,10 +34,10 @@
diff --git a/Web/Views/Vehicle/Index.cshtml b/Web/Views/Vehicle/Index.cshtml index 91d50ab..ad8bec8 100644 --- a/Web/Views/Vehicle/Index.cshtml +++ b/Web/Views/Vehicle/Index.cshtml @@ -13,7 +13,7 @@

@ViewBag.Title

- @Html.ActionLink("Add Another Vehicle", "Create", null, new{@class="btn"}) + @Html.ActionLink("Add Vehicle", "Create", null, new{@class="btn"}) @Html.ActionLink("Export All", "Export", null, new { @class = "btn" }) @Html.ActionLink(Model.Inactive ? "Show Active" : "Show Inactive", "Index", new {inactive = !Model.Inactive}, new { @class = "btn" })
@@ -26,13 +26,7 @@ Ethra Id - Model Yr - - - Make - - - Model + Year, Make, Model Type @@ -46,9 +40,12 @@ Assigned - - ODO - + + ODO + + + Next Service + @@ -61,13 +58,7 @@ @Html.DisplayTextFor(m => item.VehicleId) - @Html.DisplayTextFor(m => item.ModelYear) - - - @Html.DisplayTextFor(m => item.Make) - - - @Html.DisplayTextFor(m => item.CarModel) + @Html.DisplayTextFor(m => item.ModelYear) @Html.DisplayTextFor(m => item.Make) @Html.DisplayTextFor(m => item.CarModel) @@ -85,13 +76,20 @@ @Html.DisplayTextFor(m => item.Notes) } - - @Html.DisplayTextFor(m => item.CurrentOdometer) - + + @Html.DisplayTextFor(m => item.CurrentOdometer) + + + @Html.DisplayTextFor(m => item.NextServiceOdometer) + @if (item.IsNextServiceOverdue) + { + ! + } + @Html.ActionLink("Reminder", "Index", "ServiceReminder", new { vehicleId = item.VehicleId }, new { @class = "btn btn-mini pull-right" }) +
@Html.ActionLink("Details", "Details", new { id = item.VehicleId }, new { @class = "btn btn-mini" }) - @Html.ActionLink("Svc Reminder", "Index", "ServiceReminder", new { vehicleId = item.VehicleId }, new { @class = "btn btn-mini" })
diff --git a/Web/Views/VehicleService/Index.cshtml b/Web/Views/VehicleService/Index.cshtml index 28c0312..b6a1750 100644 --- a/Web/Views/VehicleService/Index.cshtml +++ b/Web/Views/VehicleService/Index.cshtml @@ -26,22 +26,50 @@ }
+
+

Upcoming Services

+ + + + + + + + +@foreach (var sr in Model.UpcomingServiceReminders) +{ + + + + + + + +} +
Vehicle IDODONext ServiceDescription
@Html.DisplayTextFor(m => sr.VehicleId)@Html.DisplayTextFor(m => sr.CurrentOdometer)@Html.DisplayTextFor(m => sr.TargetOdometer) + @if (sr.IsServiceOverdue) + { + ! + } + @Html.DisplayTextFor(m => sr.Description)@Html.ActionLink("Add Service", "Create", "VehicleService", new { vehicleId = sr.VehicleId }, new { @class = "btn btn-mini" })
+
+

@ViewBag.Title

- @Html.ActionLink("Add Service", "Create", null, new{@class="btn"}) - @Html.ActionLink("Export", "Export", queryParams, new{@class="btn"}) + @Html.ActionLink("Add Service", "Create", null, new { @class = "btn" }) + @Html.ActionLink("Export", "Export", queryParams, new { @class = "btn" })
@grid.GetHtml(columns: - grid.Columns( - grid.Column("InvoiceDate", "Invoice Date", item => item.InvoiceDate.ToString("d")), - grid.Column("VehicleID", "Vehicle Id"), - grid.Column("ServiceCenterName", "Service Center Name"), - grid.Column("Price", "Price"), - grid.Column("Description", "Description"), - grid.Column(format: - @
+ grid.Columns( + grid.Column("InvoiceDate", "Invoice Date", item => item.InvoiceDate.ToString("d")), + grid.Column("VehicleID", "Vehicle Id"), + grid.Column("ServiceCenterName", "Service Center Name"), + grid.Column("Price", "Price"), + grid.Column("Description", "Description"), + grid.Column(format: + @
@Html.ActionLink("Details", "Details", new { id = item.VehicleServiceId }, new { @class = "btn btn-mini" })
) ), diff --git a/Web/Views/VehicleService/UpdateServiceReminders.cshtml b/Web/Views/VehicleService/UpdateServiceReminders.cshtml new file mode 100644 index 0000000..4bf1f41 --- /dev/null +++ b/Web/Views/VehicleService/UpdateServiceReminders.cshtml @@ -0,0 +1,32 @@ +@model MileageTraker.Web.ViewModels.VehicleService.UpdateServiceRemindersViewModel + +@{ + ViewBag.Title = "Update Service Reminders"; +} + +@Html.Partial("_StatusMessage") + +

@ViewBag.Title

+
Add the next service for this vehicle and remove existing reminders.
+ +@using (Html.BeginForm("UpdateServiceReminders", "VehicleService", FormMethod.Post, new { @class = "form-horizontal well center-content" })) +{ + @Html.Partial("_ValidationSummary") + @Html.HiddenFor(m => m.VehicleId) + if (Model.DeleteServiceReminders.Available.Count > 0) + { +
+ Delete Reminders for this Vehicle? + @Html.EditorFor(m => m.DeleteServiceReminders) +
+ } +
+ Add a new Reminder? + @Html.DisplayFor(m => m.CurrentOdometer) + @Html.EditorFor(m => m.TargetOdometer) + @Html.EditorFor(m => m.Description) +
+
+ +
+} diff --git a/Web/Web.csproj b/Web/Web.csproj index 6fb49aa..7099dce 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -146,6 +146,7 @@ + @@ -268,6 +269,7 @@ + @@ -350,6 +352,7 @@ +