From 829d84d73ede08e1bc0d41e74bbaeac225a82bfa Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Tue, 22 Sep 2015 21:11:47 -0400 Subject: [PATCH] Fuel Log Match --- Web/Content/Site.css | 16 + Web/Controllers/FuelLogController.cs | 50 +- Web/Controllers/LogController.cs | 17 +- Web/DAL/DataService.cs | 79 ++- Web/Models/Log.cs | 2 + Web/Scripts/jquery.numeric.js | 477 ++++++++++-------- .../FuelLog/FuelLogIndexViewModel.cs | 53 ++ Web/ViewModels/FuelLog/FuelLogViewModel.cs | 19 + Web/ViewModels/FuelLog/LogMatchViewModel.cs | 72 +++ Web/ViewModels/FuelLog/MatchViewModel.cs | 11 + Web/ViewModels/FuelLog/ResultsViewModel.cs | 6 +- Web/Views/FuelLog/Import.cshtml | 2 +- Web/Views/FuelLog/Index.cshtml | 14 +- Web/Views/FuelLog/Match.cshtml | 137 +++++ .../FuelLog/MatchLogViewModelPartial.cshtml | 17 + Web/Views/Shared/_Layout.login.cshtml | 3 +- Web/Web.csproj | 5 + Web/packages.config | 1 + 18 files changed, 748 insertions(+), 233 deletions(-) create mode 100644 Web/ViewModels/FuelLog/FuelLogIndexViewModel.cs create mode 100644 Web/ViewModels/FuelLog/LogMatchViewModel.cs create mode 100644 Web/ViewModels/FuelLog/MatchViewModel.cs create mode 100644 Web/Views/FuelLog/Match.cshtml create mode 100644 Web/Views/FuelLog/MatchLogViewModelPartial.cshtml diff --git a/Web/Content/Site.css b/Web/Content/Site.css index 5e48a99..bc3b7ea 100644 --- a/Web/Content/Site.css +++ b/Web/Content/Site.css @@ -157,6 +157,22 @@ dl.inline { padding-left: 10px; } +td.nomatch { + background-color: #f89406 !IMPORTANT; + color: white !IMPORTANT; +} + +td.match { + background-color: #468847 !IMPORTANT; + color: white !IMPORTANT; +} + +#fuellogs th, +#currentlymatchedlog th, +#matchedlogs th { + width: 16.6% +} + @media print { header, footer, diff --git a/Web/Controllers/FuelLogController.cs b/Web/Controllers/FuelLogController.cs index a803836..969f5ad 100644 --- a/Web/Controllers/FuelLogController.cs +++ b/Web/Controllers/FuelLogController.cs @@ -6,7 +6,6 @@ using MileageTraker.Web.Attributes; using MileageTraker.Web.DAL; using MileageTraker.Web.Utility; using MileageTraker.Web.ViewModels.FuelLog; -using MileageTraker.Web.ViewModels.Log; using ImportUploadViewModel = MileageTraker.Web.ViewModels.FuelLog.ImportUploadViewModel; namespace MileageTraker.Web.Controllers @@ -34,6 +33,7 @@ namespace MileageTraker.Web.Controllers } var fuelLogs = DataService.GetFuelLogs(); + //fuelLogs.OrderBy(f => f.FuelLogId); //var filteredLogs = @@ -41,7 +41,10 @@ namespace MileageTraker.Web.Controllers // orderby log. descending // select log).ToList(); - var viewModel = new ResultsViewModel(fuelLogs, query, CustomExtensions.YearMonthList(validLogYearMonths)); + var flvm = from fl in fuelLogs.ToList() + select new FuelLogIndexViewModel(fl); + + var viewModel = new ResultsViewModel(flvm, query, CustomExtensions.YearMonthList(validLogYearMonths)); //Session.Add("FuelLogPage", Request.Url.PathAndQuery); @@ -51,10 +54,49 @@ namespace MileageTraker.Web.Controllers public ViewResult Details(int id) { var fuelLog = DataService.GetFuelLog(id); - var vm = new FuelLogViewModel(fuelLog); + var vm = new FuelLogViewModel(fuelLog) { VehicleId = DataService.GetVehicleIdByTag(fuelLog.TagNumber) }; return View(vm); } + public ViewResult Match(int id) + { + var fuelLog = DataService.GetFuelLog(id); + var fuelLogViewModel = new FuelLogViewModel(fuelLog) {VehicleId = DataService.GetVehicleIdByTag(fuelLog.TagNumber)}; + + var logs = from l in DataService.GetPossibleMatchingLogs(fuelLog).ToList() + where l != fuelLog.Log + let v = DataService.GetVehicle(l.VehicleId) + let vm = new LogMatchViewModel(l, v, fuelLogViewModel) + orderby vm.MismatchCount ascending + select vm; + + var matchViewModel = + new MatchViewModel + { + FuelLog = fuelLogViewModel, + MatchedLogs = logs.ToList() + }; + + if (fuelLog.Log != null) + matchViewModel.CurrentlyMatchedLog = new LogMatchViewModel(fuelLog.Log, DataService.GetVehicle(fuelLog.Log.VehicleId), fuelLogViewModel); + + // When creating a new Log to match the fuel log, this holds the ID between actions + TempData["FuelLogId"] = id; + + return View(matchViewModel); + } + + [HttpPost] + public ActionResult Match(int fuelLogId, int logId) + { + var fuelLog = DataService.GetFuelLog(fuelLogId); + var log = DataService.GetLog(logId); + fuelLog.Log = log; + DataService.UpdateFuelLog(fuelLog); + + return RedirectToAction("Match", new {id = fuelLogId}); + } + #region Import public ActionResult ImportUpload() @@ -114,7 +156,7 @@ namespace MileageTraker.Web.Controllers } [HttpPost] - public ActionResult Match(int fuelLogId) + public ActionResult ImportMatch(int fuelLogId) { var fuelLog = DataService.GetFuelLog(fuelLogId); if (fuelLog == null) diff --git a/Web/Controllers/LogController.cs b/Web/Controllers/LogController.cs index 64c459b..8d4c399 100644 --- a/Web/Controllers/LogController.cs +++ b/Web/Controllers/LogController.cs @@ -45,7 +45,6 @@ namespace MileageTraker.Web.Controllers orderby log.Created descending select log).ToList(); - var viewModel = new LogResultsViewModel(filteredLogs, query, CustomExtensions.YearMonthList(validLogYearMonths)); Session.Add("LogPage", Request.Url.PathAndQuery); @@ -162,7 +161,21 @@ namespace MileageTraker.Web.Controllers DataService.AddLog(log); TempData["StatusMessage"] = "Log created"; - return RedirectToAction("Index"); + + if (TempData.ContainsKey("FuelLogId")) + { + // update fuel log + var fuelLogId = (int)TempData["FuelLogId"]; + var fuelLog = DataService.GetFuelLog(fuelLogId); + fuelLog.Log = log; + DataService.UpdateFuelLog(fuelLog); + + // return to the fuel log match page + TempData["StatusMessage"] = "Log created and matched to the fuel log"; + return RedirectToAction("Match", "FuelLog", new {id = fuelLogId}); + } + + return RedirectToAction("Index"); } viewModel.Purpose.Available = GetPurposeTypesSelectList(); diff --git a/Web/DAL/DataService.cs b/Web/DAL/DataService.cs index a1846e6..a8406f7 100644 --- a/Web/DAL/DataService.cs +++ b/Web/DAL/DataService.cs @@ -99,6 +99,12 @@ namespace MileageTraker.Web.DAL _db.Entry(nextLog).State = EntityState.Modified; } + // remove the reference to the log from any fuellogs + foreach (var fuelLog in log.FuelLogs) + { + fuelLog.Log = null; + } + _db.Logs.Remove(log); _db.SaveChanges(); @@ -409,6 +415,14 @@ namespace MileageTraker.Web.DAL return _db.Vehicles.Find(id); } + public string GetVehicleIdByTag(string tagNumber) + { + var vehicle = GetVehicleByTag(tagNumber); + if (vehicle != null) + return vehicle.VehicleId; + return null; + } + public Vehicle GetVehicleByTag(string tagNumber) { var vehicle = GetVehicles().FirstOrDefault(v => v.TagNumber == tagNumber); @@ -672,13 +686,70 @@ namespace MileageTraker.Web.DAL log.Date.Day == fuelLog.Date.Day ); logs = logs.Where(log => log.GasPurchased == fuelLog.GasPurchased); - - //logs = logs.Where(log => log.User.FullName == fuelLog.DriverFullName); - //logs = logs.Where(log => log.EndOdometer == fuelLog.Odometer); - + return logs.FirstOrDefault(); } + public IEnumerable GetPossibleMatchingLogs(FuelLog fuelLog) + { + const int dateRange = 3; + const double gasRange = 1; + const double odometerRange = 100; + var gasVehicleQuery = GetLogs(); + + // matching by Date, Gas Purchased (gallons), and Vehicle + + var vehicle = GetVehicleByTag(fuelLog.TagNumber); + if (vehicle != null) + gasVehicleQuery = gasVehicleQuery.Where(log => log.VehicleId == vehicle.VehicleId); + + var futureDate = fuelLog.Date.AddDays(dateRange); + var pastDate = fuelLog.Date.AddDays(-dateRange); + + gasVehicleQuery = gasVehicleQuery.Where(log => + log.Date.Year <= futureDate.Year && + log.Date.Month <= futureDate.Month && + log.Date.Day <= futureDate.Day && + log.Date.Year >= pastDate.Year && + log.Date.Month >= pastDate.Month && + log.Date.Day >= pastDate.Day + ); + + var gasMax = fuelLog.GasPurchased + gasRange; + var gasMin = fuelLog.GasPurchased - gasRange; + gasVehicleQuery = gasVehicleQuery.Where(log => log.GasPurchased <= gasMax && log.GasPurchased >= gasMin); + + var driverQuery = + GetLogs() + .Where(log => log.User.FullName.Contains(fuelLog.DriverFullName)) + .Where(log => log.GasPurchased > 0) + .Where(log => + log.Date.Year <= futureDate.Year && + log.Date.Month <= futureDate.Month && + log.Date.Day <= futureDate.Day && + log.Date.Year >= pastDate.Year && + log.Date.Month >= pastDate.Month && + log.Date.Day >= pastDate.Day + ); + + var odoMax = fuelLog.Odometer + odometerRange; + var odoMin = fuelLog.Odometer - odometerRange; + var odometerQuery = + GetLogs() + .Where(log => log.EndOdometer > odoMin && log.EndOdometer < odoMax) + .Where(log => log.GasPurchased > 0) + .Where(log => + log.Date.Year <= futureDate.Year && + log.Date.Month <= futureDate.Month && + log.Date.Day <= futureDate.Day && + log.Date.Year >= pastDate.Year && + log.Date.Month >= pastDate.Month && + log.Date.Day >= pastDate.Day + ); + + return gasVehicleQuery.Union(driverQuery).Union(odometerQuery); + } + public IList GetValidFuelLogMonths() { var months = diff --git a/Web/Models/Log.cs b/Web/Models/Log.cs index 26626da..8b9b8ae 100644 --- a/Web/Models/Log.cs +++ b/Web/Models/Log.cs @@ -63,6 +63,8 @@ namespace MileageTraker.Web.Models [HiddenInput(DisplayValue = false)] public virtual Log VehiclePreviousLog { get; set; } + public virtual ICollection FuelLogs { get; set; } + public Log() { LogType = new MileageLogTypeWrapper(); diff --git a/Web/Scripts/jquery.numeric.js b/Web/Scripts/jquery.numeric.js index f57d32f..ed3ed2f 100644 --- a/Web/Scripts/jquery.numeric.js +++ b/Web/Scripts/jquery.numeric.js @@ -1,239 +1,296 @@ -/* -* -* Copyright (c) 2006-2011 Sam Collett (http://www.texotela.co.uk) -* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) -* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. -* -* Version 1.3 -* Demo: http://www.texotela.co.uk/code/jquery/numeric/ -* -*/ +/* + * + * Copyright (c) 2006-2014 Sam Collett (http://www.texotela.co.uk) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * Version 1.4.1 + * Demo: http://www.texotela.co.uk/code/jquery/numeric/ + * + */ (function ($) { - /* + /* * Allows only valid characters to be entered into input boxes. * Note: fixes value when pasting via Ctrl+V, but not when using the mouse to paste - * side-effect: Ctrl+A does not work, though you can still use the mouse to select (or double-click to select all) + * side-effect: Ctrl+A does not work, though you can still use the mouse to select (or double-click to select all) * * @name numeric * @param config { decimal : "." , negative : true } * @param callback A function that runs if the number is not valid (fires onblur) * @author Sam Collett (http://www.texotela.co.uk) * @example $(".numeric").numeric(); - * @example $(".numeric").numeric(","); // use , as separater + * @example $(".numeric").numeric(","); // use , as separator * @example $(".numeric").numeric({ decimal : "," }); // use , as separator * @example $(".numeric").numeric({ negative : false }); // do not allow negative values + * @example $(".numeric").numeric({ decimalPlaces : 2 }); // only allow 2 decimal places * @example $(".numeric").numeric(null, callback); // use default values, pass on the 'callback' function * */ - $.fn.numeric = function (config, callback) { - if (typeof config === 'boolean') { - config = { decimal: config }; - } - config = config || {}; - // if config.negative undefined, set to true (default is to allow negative numbers) - if (typeof config.negative == "undefined") config.negative = true; - // set decimal point - var decimal = (config.decimal === false) ? "" : config.decimal || "."; - // allow negatives - var negative = (config.negative === true) ? true : false; - // callback function - var callback = typeof callback == "function" ? callback : function () { }; - // set data and methods - return this.data("numeric.decimal", decimal).data("numeric.negative", negative).data("numeric.callback", callback).keypress($.fn.numeric.keypress).keyup($.fn.numeric.keyup).blur($.fn.numeric.blur); - } + $.fn.numeric = function (config, callback) { + if (typeof config === 'boolean') { + config = { decimal: config, negative: true, decimalPlaces: -1 }; + } + config = config || {}; + // if config.negative undefined, set to true (default is to allow negative numbers) + if (typeof config.negative == "undefined") { config.negative = true; } + // set decimal point + var decimal = (config.decimal === false) ? "" : config.decimal || "."; + // allow negatives + var negative = (config.negative === true) ? true : false; + // set decimal places + var decimalPlaces = (typeof config.decimalPlaces == "undefined") ? -1 : config.decimalPlaces; + // callback function + callback = (typeof (callback) == "function" ? callback : function () { }); + // set data and methods + return this.data("numeric.decimal", decimal).data("numeric.negative", negative).data("numeric.callback", callback).data("numeric.decimalPlaces", decimalPlaces).keypress($.fn.numeric.keypress).keyup($.fn.numeric.keyup).blur($.fn.numeric.blur); + }; - $.fn.numeric.keypress = function (e) { - // get decimal character and determine if negatives are allowed - var decimal = $.data(this, "numeric.decimal"); - var negative = $.data(this, "numeric.negative"); - // get the key that was pressed - var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0; - // allow enter/return key (only when in an input box) - if (key == 13 && this.nodeName.toLowerCase() == "input") { - return true; - } - else if (key == 13) { - return false; - } - var allow = false; - // allow Ctrl+A - if ((e.ctrlKey && key == 97 /* firefox */) || (e.ctrlKey && key == 65) /* opera */) return true; - // allow Ctrl+X (cut) - if ((e.ctrlKey && key == 120 /* firefox */) || (e.ctrlKey && key == 88) /* opera */) return true; - // allow Ctrl+C (copy) - if ((e.ctrlKey && key == 99 /* firefox */) || (e.ctrlKey && key == 67) /* opera */) return true; - // allow Ctrl+Z (undo) - if ((e.ctrlKey && key == 122 /* firefox */) || (e.ctrlKey && key == 90) /* opera */) return true; - // allow or deny Ctrl+V (paste), Shift+Ins - if ((e.ctrlKey && key == 118 /* firefox */) || (e.ctrlKey && key == 86) /* opera */ - || (e.shiftKey && key == 45)) return true; - // if a number was not pressed - if (key < 48 || key > 57) { - /* '-' only allowed at start and if negative numbers allowed */ - if (this.value.indexOf("-") != 0 && negative && key == 45 && (this.value.length == 0 || ($.fn.getSelectionStart(this)) == 0)) return true; - /* only one decimal separator allowed */ - if (decimal && key == decimal.charCodeAt(0) && this.value.indexOf(decimal) != -1) { - allow = false; + $.fn.numeric.keypress = function (e) { + // get decimal character and determine if negatives are allowed + var decimal = $.data(this, "numeric.decimal"); + var negative = $.data(this, "numeric.negative"); + var decimalPlaces = $.data(this, "numeric.decimalPlaces"); + // get the key that was pressed + var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0; + // allow enter/return key (only when in an input box) + if (key == 13 && this.nodeName.toLowerCase() == "input") { + return true; + } + else if (key == 13) { + return false; + } + var allow = false; + // allow Ctrl+A + if ((e.ctrlKey && key == 97 /* firefox */) || (e.ctrlKey && key == 65) /* opera */) { return true; } + // allow Ctrl+X (cut) + if ((e.ctrlKey && key == 120 /* firefox */) || (e.ctrlKey && key == 88) /* opera */) { return true; } + // allow Ctrl+C (copy) + if ((e.ctrlKey && key == 99 /* firefox */) || (e.ctrlKey && key == 67) /* opera */) { return true; } + // allow Ctrl+Z (undo) + if ((e.ctrlKey && key == 122 /* firefox */) || (e.ctrlKey && key == 90) /* opera */) { return true; } + // allow or deny Ctrl+V (paste), Shift+Ins + if ((e.ctrlKey && key == 118 /* firefox */) || (e.ctrlKey && key == 86) /* opera */ || + (e.shiftKey && key == 45)) { return true; } + // if a number was not pressed + if (key < 48 || key > 57) { + var value = $(this).val(); + /* '-' only allowed at start and if negative numbers allowed */ + if ($.inArray('-', value.split('')) !== 0 && negative && key == 45 && (value.length === 0 || parseInt($.fn.getSelectionStart(this), 10) === 0)) { return true; } + /* only one decimal separator allowed */ + if (decimal && key == decimal.charCodeAt(0) && $.inArray(decimal, value.split('')) != -1) { + allow = false; + } + // check for other keys that have special purposes + if ( + key != 8 /* backspace */ && + key != 9 /* tab */ && + key != 13 /* enter */ && + key != 35 /* end */ && + key != 36 /* home */ && + key != 37 /* left */ && + key != 39 /* right */ && + key != 46 /* del */ + ) { + allow = false; + } + else { + // for detecting special keys (listed above) + // IE does not support 'charCode' and ignores them in keypress anyway + if (typeof e.charCode != "undefined") { + // special keys have 'keyCode' and 'which' the same (e.g. backspace) + if (e.keyCode == e.which && e.which !== 0) { + allow = true; + // . and delete share the same code, don't allow . (will be set to true later if it is the decimal point) + if (e.which == 46) { allow = false; } + } + // or keyCode != 0 and 'charCode'/'which' = 0 + else if (e.keyCode !== 0 && e.charCode === 0 && e.which === 0) { + allow = true; + } } - // check for other keys that have special purposes - if ( - key != 8 /* backspace */ && - key != 9 /* tab */ && - key != 13 /* enter */ && - key != 35 /* end */ && - key != 36 /* home */ && - key != 37 /* left */ && - key != 39 /* right */ && - key != 46 /* del */ - ) { - allow = false; + } + // if key pressed is the decimal and it is not already in the field + if (decimal && key == decimal.charCodeAt(0)) { + if ($.inArray(decimal, value.split('')) == -1) { + allow = true; } else { - // for detecting special keys (listed above) - // IE does not support 'charCode' and ignores them in keypress anyway - if (typeof e.charCode != "undefined") { - // special keys have 'keyCode' and 'which' the same (e.g. backspace) - if (e.keyCode == e.which && e.which != 0) { - allow = true; - // . and delete share the same code, don't allow . (will be set to true later if it is the decimal point) - if (e.which == 46) allow = false; - } - // or keyCode != 0 and 'charCode'/'which' = 0 - else if (e.keyCode != 0 && e.charCode == 0 && e.which == 0) { - allow = true; - } - } + allow = false; } - // if key pressed is the decimal and it is not already in the field - if (decimal && key == decimal.charCodeAt(0)) { - if (this.value.indexOf(decimal) == -1) { - allow = true; - } - else { - allow = false; - } + } + } + else { + allow = true; + // remove extra decimal places + if (decimal && decimalPlaces > 0) { + var dot = $.inArray(decimal, $(this).val().split('')); + if (dot >= 0 && $(this).val().length > dot + decimalPlaces) { + allow = false; } - } - else { - allow = true; - } - return allow; - } + } - $.fn.numeric.keyup = function (e) { - var val = this.value; - if (val.length > 0) { - // get carat (cursor) position - var carat = $.fn.getSelectionStart(this); - // get decimal character and determine if negatives are allowed - var decimal = $.data(this, "numeric.decimal"); - var negative = $.data(this, "numeric.negative"); + } + return allow; + }; - // prepend a 0 if necessary - if (decimal != "") { - // find decimal point - var dot = val.indexOf(decimal); - // if dot at start, add 0 before - if (dot == 0) { - this.value = "0" + val; - } - // if dot at position 1, check if there is a - symbol before it - if (dot == 1 && val.charAt(0) == "-") { - this.value = "-0" + val.substring(1); - } - val = this.value; + $.fn.numeric.keyup = function (e) { + var val = $(this).val(); + if (val && val.length > 0) { + // get carat (cursor) position + var carat = $.fn.getSelectionStart(this); + var selectionEnd = $.fn.getSelectionEnd(this); + // get decimal character and determine if negatives are allowed + var decimal = $.data(this, "numeric.decimal"); + var negative = $.data(this, "numeric.negative"); + var decimalPlaces = $.data(this, "numeric.decimalPlaces"); + + // prepend a 0 if necessary + if (decimal !== "" && decimal !== null) { + // find decimal point + var dot = $.inArray(decimal, val.split('')); + // if dot at start, add 0 before + if (dot === 0) { + this.value = "0" + val; + carat++; + selectionEnd++; } - - // if pasted in, only allow the following characters - var validChars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '-', decimal]; - // get length of the value (to loop through) - var length = val.length; - // loop backwards (to prevent going out of bounds) - for (var i = length - 1; i >= 0; i--) { - var ch = val.charAt(i); - // remove '-' if it is in the wrong place - if (i != 0 && ch == "-") { - val = val.substring(0, i) + val.substring(i + 1); - } - // remove character if it is at the start, a '-' and negatives aren't allowed - else if (i == 0 && !negative && ch == "-") { - val = val.substring(1); - } - var validChar = false; - // loop through validChars - for (var j = 0; j < validChars.length; j++) { - // if it is valid, break out the loop - if (ch == validChars[j]) { - validChar = true; - break; - } - } - // if not a valid character, or a space, remove - if (!validChar || ch == " ") { - val = val.substring(0, i) + val.substring(i + 1); - } + // if dot at position 1, check if there is a - symbol before it + if (dot == 1 && val.charAt(0) == "-") { + this.value = "-0" + val.substring(1); + carat++; + selectionEnd++; } - // remove extra decimal characters - var firstDecimal = val.indexOf(decimal); - if (firstDecimal > 0) { - for (var i = length - 1; i > firstDecimal; i--) { - var ch = val.charAt(i); - // remove decimal character - if (ch == decimal) { - val = val.substring(0, i) + val.substring(i + 1); - } - } + val = this.value; + } + + // if pasted in, only allow the following characters + var validChars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '-', decimal]; + // get length of the value (to loop through) + var length = val.length; + // loop backwards (to prevent going out of bounds) + for (var i = length - 1; i >= 0; i--) { + var ch = val.charAt(i); + // remove '-' if it is in the wrong place + if (i !== 0 && ch == "-") { + val = val.substring(0, i) + val.substring(i + 1); } - // set the value and prevent the cursor moving to the end - this.value = val; - $.fn.setSelection(this, carat); - } - } - - $.fn.numeric.blur = function () { - var decimal = $.data(this, "numeric.decimal"); - var callback = $.data(this, "numeric.callback"); - var val = this.value; - if (val != "") { - var re = new RegExp("^\\d+$|\\d*" + decimal + "\\d+"); - if (!re.exec(val)) { - callback.apply(this); + // remove character if it is at the start, a '-' and negatives aren't allowed + else if (i === 0 && !negative && ch == "-") { + val = val.substring(1); } - } - } - - $.fn.removeNumeric = function () { - return this.data("numeric.decimal", null).data("numeric.negative", null).data("numeric.callback", null).unbind("keypress", $.fn.numeric.keypress).unbind("blur", $.fn.numeric.blur); - } - - // Based on code from http://javascript.nwbox.com/cursor_position/ (Diego Perini ) - $.fn.getSelectionStart = function (o) { - if (o.createTextRange) { - var r = document.selection.createRange().duplicate(); - r.moveEnd('character', o.value.length); - if (r.text == '') return o.value.length; - return o.value.lastIndexOf(r.text); - } else return o.selectionStart; - } - - // set the selection, o is the object (input), p is the position ([start, end] or just start) - $.fn.setSelection = function (o, p) { - // if p is number, start and end are the same - if (typeof p == "number") p = [p, p]; - // only set if p is an array of length 2 - if (p && p.constructor == Array && p.length == 2) { - if (o.createTextRange) { - var r = o.createTextRange(); - r.collapse(true); - r.moveStart('character', p[0]); - r.moveEnd('character', p[1]); - r.select(); + var validChar = false; + // loop through validChars + for (var j = 0; j < validChars.length; j++) { + // if it is valid, break out the loop + if (ch == validChars[j]) { + validChar = true; + break; + } } - else if (o.setSelectionRange) { - o.focus(); - o.setSelectionRange(p[0], p[1]); + // if not a valid character, or a space, remove + if (!validChar || ch == " ") { + val = val.substring(0, i) + val.substring(i + 1); } - } - } + } + // remove extra decimal characters + var firstDecimal = $.inArray(decimal, val.split('')); + if (firstDecimal > 0) { + for (var k = length - 1; k > firstDecimal; k--) { + var chch = val.charAt(k); + // remove decimal character + if (chch == decimal) { + val = val.substring(0, k) + val.substring(k + 1); + } + } + } + + // remove extra decimal places + if (decimal && decimalPlaces > 0) { + var dot = $.inArray(decimal, val.split('')); + if (dot >= 0) { + val = val.substring(0, dot + decimalPlaces + 1); + selectionEnd = Math.min(val.length, selectionEnd); + } + } + // set the value and prevent the cursor moving to the end + this.value = val; + $.fn.setSelection(this, [carat, selectionEnd]); + } + }; + + $.fn.numeric.blur = function () { + var decimal = $.data(this, "numeric.decimal"); + var callback = $.data(this, "numeric.callback"); + var negative = $.data(this, "numeric.negative"); + var val = this.value; + if (val !== "") { + var re = new RegExp(negative ? "-?" : "" + "^\\d+$|^\\d*" + decimal + "\\d+$"); + if (!re.exec(val)) { + callback.apply(this); + } + } + }; + + $.fn.removeNumeric = function () { + return this.data("numeric.decimal", null).data("numeric.negative", null).data("numeric.callback", null).data("numeric.decimalPlaces", null).unbind("keypress", $.fn.numeric.keypress).unbind("keyup", $.fn.numeric.keyup).unbind("blur", $.fn.numeric.blur); + }; + + // Based on code from http://javascript.nwbox.com/cursor_position/ (Diego Perini ) + $.fn.getSelectionStart = function (o) { + if (o.type === "number") { + return undefined; + } + else if (o.createTextRange && document.selection) { + var r = document.selection.createRange().duplicate(); + r.moveEnd('character', o.value.length); + if (r.text == '') return o.value.length; + + return Math.max(0, o.value.lastIndexOf(r.text)); + } else { + try { return o.selectionStart; } + catch (e) { return 0; } + } + }; + + // Based on code from http://javascript.nwbox.com/cursor_position/ (Diego Perini ) + $.fn.getSelectionEnd = function (o) { + if (o.type === "number") { + return undefined; + } + else if (o.createTextRange && document.selection) { + var r = document.selection.createRange().duplicate() + r.moveStart('character', -o.value.length) + return r.text.length + } else return o.selectionEnd + } + + // set the selection, o is the object (input), p is the position ([start, end] or just start) + $.fn.setSelection = function (o, p) { + // if p is number, start and end are the same + if (typeof p == "number") { p = [p, p]; } + // only set if p is an array of length 2 + if (p && p.constructor == Array && p.length == 2) { + if (o.type === "number") { + o.focus(); + } + else if (o.createTextRange) { + var r = o.createTextRange(); + r.collapse(true); + r.moveStart('character', p[0]); + r.moveEnd('character', p[1] - p[0]); + r.select(); + } + else { + o.focus(); + try { + if (o.setSelectionRange) { + o.setSelectionRange(p[0], p[1]); + } + } catch (e) { + } + } + } + }; + +})(jQuery); -})(jQuery); \ No newline at end of file diff --git a/Web/ViewModels/FuelLog/FuelLogIndexViewModel.cs b/Web/ViewModels/FuelLog/FuelLogIndexViewModel.cs new file mode 100644 index 0000000..e5c5081 --- /dev/null +++ b/Web/ViewModels/FuelLog/FuelLogIndexViewModel.cs @@ -0,0 +1,53 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; +using AutoMapper; + +namespace MileageTraker.Web.ViewModels.FuelLog +{ + public class FuelLogIndexViewModel + { + [HiddenInput(DisplayValue = false)] + public int FuelLogId { get; set; } + + [DataType(DataType.Date)] + [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")] + public DateTime Date { get; set; } + + [StringLength(128)] + [Display(Name = "Driver Name")] + public string DriverFullName { get; set; } + + [Display(Name = "Tag#")] + public string TagNumber { get; set; } + + public int Odometer { get; set; } + + [Display(Name = "MPG")] + public double MPG { get; set; } + + [Display(Name = "Gas Purchased")] + public double GasPurchased { get; set; } + + [Display(Name = "Total Price")] + public decimal TotalPrice { get; set; } + + // Matched log + [HiddenInput(DisplayValue = false)] + public int? LogId { get; set; } + + static FuelLogIndexViewModel() + { + Mapper.CreateMap() + .ForMember(dest => dest.LogId, + opt => opt.ResolveUsing( + fl => fl.Log != null ? (int?)fl.Log.LogId : null + )); + } + + public FuelLogIndexViewModel(Models.FuelLog fuelLog) + { + Mapper.Map(fuelLog, this); + } + } +} \ No newline at end of file diff --git a/Web/ViewModels/FuelLog/FuelLogViewModel.cs b/Web/ViewModels/FuelLog/FuelLogViewModel.cs index a29adfa..3799903 100644 --- a/Web/ViewModels/FuelLog/FuelLogViewModel.cs +++ b/Web/ViewModels/FuelLog/FuelLogViewModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using AutoMapper; +using MileageTraker.Web.ViewModels.Log; namespace MileageTraker.Web.ViewModels.FuelLog { @@ -24,6 +25,9 @@ namespace MileageTraker.Web.ViewModels.FuelLog [Display(Name = "Tag#")] public string TagNumber { get; set; } + [HiddenInput(DisplayValue = false)] + public string VehicleId { get; set; } + [Required] public int Odometer { get; set; } @@ -54,6 +58,14 @@ namespace MileageTraker.Web.ViewModels.FuelLog opt => opt.ResolveUsing( fl => fl.Log != null ? (int?)fl.Log.LogId : null )); + + Mapper.CreateMap() + .ForMember(dest => dest.EndOdometer, opt => opt.MapFrom(src => src.Odometer)) + .ForMember(dest => dest.CityName, opt => opt.MapFrom(src => src.CityName)) + .ForMember(dest => dest.GasPurchased, opt => opt.MapFrom(src => src.GasPurchased)) + .ForMember(dest => dest.UserFullName, opt => opt.MapFrom(src => src.DriverFullName)) + .ForMember(dest => dest.VehicleId, opt => opt.MapFrom(src => src.VehicleId)) + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)); } public FuelLogViewModel(Models.FuelLog fuelLog) @@ -64,5 +76,12 @@ namespace MileageTraker.Web.ViewModels.FuelLog public FuelLogViewModel() { } + + public LogViewModel GetLogViewModel() + { + var logViewModel = new LogViewModel(); + Mapper.Map(this, logViewModel); + return logViewModel; + } } } \ No newline at end of file diff --git a/Web/ViewModels/FuelLog/LogMatchViewModel.cs b/Web/ViewModels/FuelLog/LogMatchViewModel.cs new file mode 100644 index 0000000..3d2472c --- /dev/null +++ b/Web/ViewModels/FuelLog/LogMatchViewModel.cs @@ -0,0 +1,72 @@ +using System; +using System.ComponentModel.DataAnnotations; +using AutoMapper; + +namespace MileageTraker.Web.ViewModels.FuelLog +{ + public class LogMatchViewModel + { + public int LogId { get; set; } + + [Display(Name = "Vehicle ID")] + public string VehicleId { get; set; } + + public string VehicleTagNumber { get; set; } + + [Display(Name = "End Odometer")] + public int EndOdometer { get; set; } + + [Display(Name = "Driver Name")] + public string UserFullName { get; set; } + + [Display(Name = "Gas Purchased")] + [DisplayFormat(DataFormatString = "{0:0.000}", ApplyFormatInEditMode = true)] + public double GasPurchased { get; set; } + + public DateTime Date { get; set; } + + // indicates an exact match for each of the fields + public bool VehicleTagMatch { get; private set; } + + public bool OdometerMatch { get; private set; } + + public bool UserFullNameMatch { get; private set; } + + public bool GasPurchasedMatch { get; private set; } + + public bool DateMatch { get; private set; } + + public int MismatchCount + { + get + { + return + (VehicleTagMatch ? 0 : 1) + + (OdometerMatch ? 0 : 1) + + (UserFullNameMatch ? 0 : 1) + + (GasPurchasedMatch ? 0 : 1) + + (DateMatch ? 0 : 1); + } + } + + static LogMatchViewModel() + { + Mapper.CreateMap(); + } + + public LogMatchViewModel(Models.Log log, Models.Vehicle vehicle, FuelLogViewModel fuelLog) + { + Mapper.Map(log, this); + VehicleTagNumber = vehicle.TagNumber; + + VehicleTagMatch = VehicleTagNumber == fuelLog.TagNumber; + OdometerMatch = log.EndOdometer == fuelLog.Odometer; + UserFullNameMatch = string.Equals(log.User.FullName, fuelLog.DriverFullName, StringComparison.InvariantCultureIgnoreCase); + GasPurchasedMatch = log.GasPurchased == fuelLog.GasPurchased; + DateMatch = + log.Date.Year == fuelLog.Date.Year + && log.Date.Month == fuelLog.Date.Month + && log.Date.Day == fuelLog.Date.Day; + } + } +} \ No newline at end of file diff --git a/Web/ViewModels/FuelLog/MatchViewModel.cs b/Web/ViewModels/FuelLog/MatchViewModel.cs new file mode 100644 index 0000000..5e9bef4 --- /dev/null +++ b/Web/ViewModels/FuelLog/MatchViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MileageTraker.Web.ViewModels.FuelLog +{ + public class MatchViewModel + { + public FuelLogViewModel FuelLog { get; set; } + public LogMatchViewModel CurrentlyMatchedLog { get; set; } + public IList MatchedLogs { get; set; } + } +} \ No newline at end of file diff --git a/Web/ViewModels/FuelLog/ResultsViewModel.cs b/Web/ViewModels/FuelLog/ResultsViewModel.cs index 231fb0d..1c8b921 100644 --- a/Web/ViewModels/FuelLog/ResultsViewModel.cs +++ b/Web/ViewModels/FuelLog/ResultsViewModel.cs @@ -5,7 +5,7 @@ namespace MileageTraker.Web.ViewModels.FuelLog { public class ResultsViewModel { - public IEnumerable Logs { get; set; } + public IEnumerable FuelLogs { get; set; } public Dictionary> AvailableYearMonths { get; set; } public IEnumerable SelectedYearMonths{get { @@ -18,9 +18,9 @@ namespace MileageTraker.Web.ViewModels.FuelLog public string Year { get; set; } public string Month { get; set; } - public ResultsViewModel(IEnumerable logs, FuelLogQueryViewModel query, Dictionary> availableYearMonths) + public ResultsViewModel(IEnumerable fuelLogs, FuelLogQueryViewModel query, Dictionary> availableYearMonths) { - Logs = logs; + FuelLogs = fuelLogs; 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/Views/FuelLog/Import.cshtml b/Web/Views/FuelLog/Import.cshtml index cb01aec..9d7f853 100644 --- a/Web/Views/FuelLog/Import.cshtml +++ b/Web/Views/FuelLog/Import.cshtml @@ -34,7 +34,7 @@ { var viewModel = Model[i]; class="complete"}> - @using (Html.BeginForm("Match", "FuelLog", FormMethod.Post)) + @using (Html.BeginForm("ImportMatch", "FuelLog", FormMethod.Post)) { } diff --git a/Web/Views/FuelLog/Index.cshtml b/Web/Views/FuelLog/Index.cshtml index 7adfad5..fb1d77d 100644 --- a/Web/Views/FuelLog/Index.cshtml +++ b/Web/Views/FuelLog/Index.cshtml @@ -3,7 +3,7 @@ @{ ViewBag.Title = "Fuel Logs"; - var grid = new WebGrid(Model.Logs, rowsPerPage: 45); + var grid = new WebGrid(Model.FuelLogs, rowsPerPage: 45); var parameters = new {Model.Year, Model.Month}; } @section Styles { @@ -43,23 +43,21 @@ grid.Columns( grid.Column("Date", format: item => item.Date.ToString("d")), grid.Column("DriverFullName", "Driver Name"), - grid.Column("TagNumber"), + grid.Column("TagNumber", "Tag Number"), grid.Column("Odometer"), grid.Column("MPG"), grid.Column("GasPurchased", "Gas Purchased", @@String.Format("{0:0.000}", item.GasPurchased)), grid.Column("TotalPrice", "Total Price", @@String.Format("{0:C}", item.TotalPrice)), grid.Column("Log", "Matched Log", @ - @(item.Log != null - ? Html.ActionLink("View Log", "Details", "Log", new {id = item.Log.LogId}, new {@class = "btn btn-mini", target="_blank"}) + @(item.LogId != null + ? Html.ActionLink("View Match", "Match", new {id = item.FuelLogId}, new {@class = "btn btn-mini", target="_blank"}) : Html.ActionLink("Match", "Match", new {id = item.FuelLogId}, new {@class = "btn btn-warning btn-mini", target="_blank"})) - ), - grid.Column("Total Results: " + Model.Logs.Count(), canSort:false, format: - @
@Html.ActionLink("Details", "Details", new { id = item.FuelLogId }, new { @class = "btn btn-mini" })
)), + )), htmlAttributes: new { @class = "table table-striped table-bordered table-hover table-condensed"}, numericLinksCount: 20 ) -@if (!Model.Logs.Any()) +@if (!Model.FuelLogs.Any()) {
No results. diff --git a/Web/Views/FuelLog/Match.cshtml b/Web/Views/FuelLog/Match.cshtml new file mode 100644 index 0000000..2068d92 --- /dev/null +++ b/Web/Views/FuelLog/Match.cshtml @@ -0,0 +1,137 @@ +@using MileageTraker.Web.ViewModels.FuelLog +@model MatchViewModel + +@{ + ViewBag.Title = "Match Fuel Log"; + var fuelLog = Model.FuelLog; + var matchedLogs = Model.MatchedLogs; + var currentlyMatchedLog = Model.CurrentlyMatchedLog; + var logViewModel = fuelLog.GetLogViewModel(); + //logViewModel.LogType = MileageLogType.GasPurchase; +} +@section Styles +{ + +} + +@Html.Partial("_StatusMessage") + +

@ViewBag.Title

+ +

+ + + + + + + + + + + + + + + + + + + + + + +
DateDriverTag NumberOdometerGas Purchased
@Html.ValueFor(x => fuelLog.Date, "{0:d}")@Html.ValueFor(x => fuelLog.DriverFullName)@Html.ValueFor(x => fuelLog.TagNumber) @if(string.IsNullOrEmpty(fuelLog.VehicleId)) { No Matching Vehicle} @Html.ValueFor(x => fuelLog.Odometer)@Html.ValueFor(x => fuelLog.GasPurchased, "{0:0.000}")
+ +@if (currentlyMatchedLog != null) +{ +
Currently Matched Mileage Log
+ + + + + + + + + + + + + + + + + + + + + + +
DateDriverTag NumberOdometerGas Purchased
@Html.ValueFor(x => currentlyMatchedLog.Date, "{0:d}")@Html.ValueFor(x => currentlyMatchedLog.UserFullName)@Html.ValueFor(x => currentlyMatchedLog.VehicleTagNumber)@Html.ValueFor(x => currentlyMatchedLog.EndOdometer)@Html.ValueFor(x => currentlyMatchedLog.GasPurchased, "{0:0.000}")@Html.ActionLink("Details", "Details", "Log", new { id = currentlyMatchedLog.LogId }, new { @class = "btn btn-mini", target="_blank" })
+} + +@if (!matchedLogs.Any()) +{ + if (currentlyMatchedLog == null) + { +

+ No matches. +

+ } + else + { +

+ No additional matches. +

+ } + +} +else +{ +
Matching Mileage Logs based on Date, Vehicle, and Gas Purchased
+ + + + + + + + + + + + + + @foreach (var matchedLog in matchedLogs) + { + + + + + + + + + + } + +
DateDriverTag NumberOdometerGas PurchasedMatch
@Html.ValueFor(x => matchedLog.Date, "{0:d}")@Html.ValueFor(x => matchedLog.UserFullName)@Html.ValueFor(x => matchedLog.VehicleTagNumber)@Html.ValueFor(x => matchedLog.EndOdometer)@Html.ValueFor(x => matchedLog.GasPurchased, "{0:0.000}")@using (Html.BeginForm("Match", "FuelLog", FormMethod.Post)) + { + + + + } +
+} +@Html.Partial("MatchLogViewModelPartial", logViewModel) diff --git a/Web/Views/FuelLog/MatchLogViewModelPartial.cshtml b/Web/Views/FuelLog/MatchLogViewModelPartial.cshtml new file mode 100644 index 0000000..a694005 --- /dev/null +++ b/Web/Views/FuelLog/MatchLogViewModelPartial.cshtml @@ -0,0 +1,17 @@ +@using MileageTraker.Web.ViewModels.Log +@model LogViewModel + +@{ + Layout = null; +} + +@using (Html.BeginForm("Create", "Log", FormMethod.Post, new {@class="form-inline"})) +{ + @Html.HiddenFor(m => m.CityName) + @Html.HiddenFor(m => m.UserFullName) + @Html.HiddenFor(m => m.EndOdometer) + @Html.HiddenFor(m => m.VehicleId) + @Html.TextBoxFor(m => m.Date, "{0:d}", new { @type = "hidden" }) + @Html.HiddenFor(m => m.GasPurchased) +

Correct mileage log doesn't exist?

+} diff --git a/Web/Views/Shared/_Layout.login.cshtml b/Web/Views/Shared/_Layout.login.cshtml index 3c15631..70eacef 100644 --- a/Web/Views/Shared/_Layout.login.cshtml +++ b/Web/Views/Shared/_Layout.login.cshtml @@ -19,7 +19,8 @@ - + + @RenderSection("Scripts", false) diff --git a/Web/Web.csproj b/Web/Web.csproj index de2ae2f..fea8238 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -230,11 +230,14 @@ + + + @@ -308,6 +311,8 @@ + + diff --git a/Web/packages.config b/Web/packages.config index 71cebb4..f9561f2 100644 --- a/Web/packages.config +++ b/Web/packages.config @@ -8,6 +8,7 @@ +