From 53ed1b3af9ba4a71ca9674b66ce66d88f3fa187f Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Mon, 22 Aug 2016 11:03:00 -0400 Subject: [PATCH] Initiate InventoyTypes from a xlxs Arrival mostly working --- .../App_Start/AutoMapperConfig.cs | 19 ++++ .../InventoryTraker.Web.Tests.csproj | 89 ++++++++++++++++++ ...ventoryTraker.Web.Tests.csproj.DotSettings | 2 + .../Models/InventoryAddForm.cs | 40 ++++++++ .../Properties/AssemblyInfo.cs | 36 +++++++ .../Documents/InventoryTypeData.xlsx | Bin 0 -> 15257 bytes .../Utilities/InventoryTypeParserTests.cs | 29 ++++++ InventoryTraker.Web.Tests/app.config | 43 +++++++++ InventoryTraker.Web.Tests/packages.config | 8 ++ .../App_Start/AutoMapperConfig.cs | 5 +- InventoryTraker.Web/App_Start/BundleConfig.cs | 4 +- InventoryTraker.Web/App_Start/SeedData.cs | 87 ++++++++++------- .../Controllers/AuthenticationController.cs | 2 +- .../Controllers/ControllerBase.cs | 14 ++- .../Controllers/InventoryController.cs | 36 +++---- .../Controllers/InventoryTypeController.cs | 27 ++++++ InventoryTraker.Web/Core/Inventory.cs | 42 +-------- InventoryTraker.Web/Core/InventoryType.cs | 25 +++++ InventoryTraker.Web/Core/Transaction.cs | 26 +++++ .../Helpers/AngularModelHelper.cs | 9 +- .../InventoryTraker.Web.csproj | 44 +++++++-- .../Models/InventoryAddForm.cs | 5 +- .../Models/InventoryTypeViewModel.cs | 4 +- .../Models/InventoryViewModel.cs | 9 +- .../Properties/PublishProfiles/ETHRA.pubxml | 33 +++++++ .../Utilities/ExcelParserBase.cs | 55 +++++++++++ .../Utilities/InventoryTypeParser.cs | 43 +++++++++ .../Views/Inventory/Index.cshtml | 4 +- .../Views/Shared/_Layout.cshtml | 6 +- .../js/inventory/InventoryAddDirective.js | 18 +++- .../js/inventory/InventoryDetailsDirective.js | 8 +- .../js/inventory/InventoryListController.js | 3 +- .../js/inventory/inventorySvc.js | 50 ---------- .../js/inventory/inventoryTypeSvc.js | 48 ++++++++++ .../templates/inventoryAdd.tmpl.cshtml | 32 +++---- .../templates/inventoryDetails.tmpl.cshtml | 34 +++++-- InventoryTraker.Web/packages.config | 4 + InventoryTraker.sln | 6 ++ global.json | 5 + wrap/InventoryTraker.Web/project.json | 52 ++++++++++ 40 files changed, 798 insertions(+), 208 deletions(-) create mode 100644 InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs create mode 100644 InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj create mode 100644 InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj.DotSettings create mode 100644 InventoryTraker.Web.Tests/Models/InventoryAddForm.cs create mode 100644 InventoryTraker.Web.Tests/Properties/AssemblyInfo.cs create mode 100644 InventoryTraker.Web.Tests/Utilities/Documents/InventoryTypeData.xlsx create mode 100644 InventoryTraker.Web.Tests/Utilities/InventoryTypeParserTests.cs create mode 100644 InventoryTraker.Web.Tests/app.config create mode 100644 InventoryTraker.Web.Tests/packages.config create mode 100644 InventoryTraker.Web/Controllers/InventoryTypeController.cs create mode 100644 InventoryTraker.Web/Core/InventoryType.cs create mode 100644 InventoryTraker.Web/Core/Transaction.cs create mode 100644 InventoryTraker.Web/Properties/PublishProfiles/ETHRA.pubxml create mode 100644 InventoryTraker.Web/Utilities/ExcelParserBase.cs create mode 100644 InventoryTraker.Web/Utilities/InventoryTypeParser.cs create mode 100644 InventoryTraker.Web/js/inventory/inventoryTypeSvc.js create mode 100644 global.json create mode 100644 wrap/InventoryTraker.Web/project.json diff --git a/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs b/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs new file mode 100644 index 0000000..c095557 --- /dev/null +++ b/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs @@ -0,0 +1,19 @@ +using Heroic.AutoMapper; + +[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(InventoryTraker.Web.Tests.AutoMapperConfig), "Configure")] +namespace InventoryTraker.Web.Tests +{ + public static class AutoMapperConfig + { + public static void Configure() + { + //NOTE: By default, the current project and all referenced projects will be scanned. + // You can customize this by passing in a lambda to filter the assemblies by name, + // like so: + //HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(x => x.Name.StartsWith("YourPrefix")); + HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(); + //If you run into issues with the maps not being located at runtime, try using this method instead: + //HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); + } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj new file mode 100644 index 0000000..c1fa6d1 --- /dev/null +++ b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + {03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B} + Library + Properties + InventoryTraker.Web.Tests + InventoryTraker.Web.Tests + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\AutoMapper.4.2.0\lib\net45\AutoMapper.dll + True + + + ..\packages\Heroic.AutoMapper.2.0.0\lib\net45\Heroic.AutoMapper.dll + True + + + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True + + + ..\packages\NUnit.3.4.1\lib\net45\nunit.framework.dll + True + + + + + + + + + + + ..\packages\WebActivatorEx.2.1.0\lib\net40\WebActivatorEx.dll + True + + + + + + + + + + + {5e5867a4-6152-4655-a04b-1737df493a41} + InventoryTraker.Web + + + + + + + PreserveNewest + + + + + \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj.DotSettings b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj.DotSettings new file mode 100644 index 0000000..306da5b --- /dev/null +++ b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/Models/InventoryAddForm.cs b/InventoryTraker.Web.Tests/Models/InventoryAddForm.cs new file mode 100644 index 0000000..2d1bb1e --- /dev/null +++ b/InventoryTraker.Web.Tests/Models/InventoryAddForm.cs @@ -0,0 +1,40 @@ +using System; +using AutoMapper; +using Heroic.AutoMapper; +using InventoryTraker.Web.Controllers; +using InventoryTraker.Web.Core; +using InventoryTraker.Web.Models; +using NUnit.Framework; + +namespace InventoryTraker.Web.Tests.Models +{ + [TestFixture] + public class InventoryAddFormTests + { + [OneTimeSetUp] + public void StartUp() + { + HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); + } + + [Test] + public void Convert() + { + var form = new InventoryAddForm + { + AddedDate = DateTime.Today, + ExpirationDate = DateTime.Today.AddDays(3), + InventoryTypeId = "1", + Memo = "My Memo", + Quantity = 32 + }; + + var inventory = Mapper.Map(form); + + Assert.That(inventory.AddedDate, Is.EqualTo(form.AddedDate)); + Assert.That(inventory.ExpirationDate, Is.EqualTo(form.ExpirationDate)); + Assert.That(inventory.Memo, Is.EqualTo(form.Memo)); + Assert.That(inventory.Quantity, Is.EqualTo(form.Quantity)); + } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/Properties/AssemblyInfo.cs b/InventoryTraker.Web.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9b7fdb8 --- /dev/null +++ b/InventoryTraker.Web.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("InventoryTraker.Web.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("InventoryTraker.Web.Tests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("03b3bdf2-b2be-42c1-8d6f-2b4e106a1d4b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/InventoryTraker.Web.Tests/Utilities/Documents/InventoryTypeData.xlsx b/InventoryTraker.Web.Tests/Utilities/Documents/InventoryTypeData.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e487126bc2d488902607661e5710a9f53bca5f20 GIT binary patch literal 15257 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 literal 0 HcmV?d00001 diff --git a/InventoryTraker.Web.Tests/Utilities/InventoryTypeParserTests.cs b/InventoryTraker.Web.Tests/Utilities/InventoryTypeParserTests.cs new file mode 100644 index 0000000..fd5df0a --- /dev/null +++ b/InventoryTraker.Web.Tests/Utilities/InventoryTypeParserTests.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using InventoryTraker.Web.Utilities; +using NUnit.Framework; + +namespace InventoryTraker.Web.Tests.Utilities +{ + [TestFixture] + public class InventoryTypeParserTests + { + private readonly string _documentFolder = + AppDomain.CurrentDomain.BaseDirectory + @"\Utilities\Documents\"; + + [Test] + public void Parse() + { + var fileInfo = new FileInfo(Path.Combine(_documentFolder, "InventoryTypeData.xlsx")); + + var parser = new InventoryTypeParser(fileInfo); + + var inventoryTypes = parser.Parse(); + + foreach (var inventoryType in inventoryTypes) + { + Console.WriteLine($"{inventoryType.Identifier} {inventoryType.Name} {inventoryType.PricePerCase}"); + } + } + } +} diff --git a/InventoryTraker.Web.Tests/app.config b/InventoryTraker.Web.Tests/app.config new file mode 100644 index 0000000..8a403d9 --- /dev/null +++ b/InventoryTraker.Web.Tests/app.config @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/packages.config b/InventoryTraker.Web.Tests/packages.config new file mode 100644 index 0000000..fadfbb0 --- /dev/null +++ b/InventoryTraker.Web.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/InventoryTraker.Web/App_Start/AutoMapperConfig.cs b/InventoryTraker.Web/App_Start/AutoMapperConfig.cs index 9fe2a4b..b8f8f0d 100644 --- a/InventoryTraker.Web/App_Start/AutoMapperConfig.cs +++ b/InventoryTraker.Web/App_Start/AutoMapperConfig.cs @@ -1,5 +1,6 @@ using Heroic.AutoMapper; using InventoryTraker.Web; +using InventoryTraker.Web.Controllers; [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(AutoMapperConfig), "Configure")] namespace InventoryTraker.Web @@ -12,9 +13,9 @@ namespace InventoryTraker.Web // You can customize this by passing in a lambda to filter the assemblies by name, // like so: //HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(x => x.Name.StartsWith("YourPrefix")); - HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(); + //HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(); //If you run into issues with the maps not being located at runtime, try using this method instead: - //HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); + HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); } } } \ No newline at end of file diff --git a/InventoryTraker.Web/App_Start/BundleConfig.cs b/InventoryTraker.Web/App_Start/BundleConfig.cs index 5daf02f..0b8e3de 100644 --- a/InventoryTraker.Web/App_Start/BundleConfig.cs +++ b/InventoryTraker.Web/App_Start/BundleConfig.cs @@ -6,7 +6,7 @@ namespace InventoryTraker.Web { public static void RegisterBundles(BundleCollection bundles) { - bundles.Add(new StyleBundle("~/Content/all.css") + bundles.Add(new StyleBundle("~/Content/all-styles") .Include("~/Content/font-awesome.css") .Include("~/Content/bootstrap.css") .Include("~/Content/sb-admin.css") @@ -14,7 +14,7 @@ namespace InventoryTraker.Web .Include("~/Content/ui-grid.css") ); - bundles.Add(new ScriptBundle("~/js/all.js") + bundles.Add(new ScriptBundle("~/js/all-javascript") .Include("~/Scripts/jquery-1.9.1.js") .Include("~/Scripts/bootstrap.js") .Include("~/Scripts/angular.js") diff --git a/InventoryTraker.Web/App_Start/SeedData.cs b/InventoryTraker.Web/App_Start/SeedData.cs index 7bf3a8f..68e2eba 100644 --- a/InventoryTraker.Web/App_Start/SeedData.cs +++ b/InventoryTraker.Web/App_Start/SeedData.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Web; using Humanizer; using InventoryTraker.Web.Core; using InventoryTraker.Web.Data; @@ -47,6 +49,10 @@ namespace InventoryTraker.Web if (!context.Inventories.Any()) { + AddInventoryTypes(context); + + context.SaveChanges(); + AddInventory(context); context.SaveChanges(); @@ -65,48 +71,59 @@ namespace InventoryTraker.Web } } + private static void AddInventoryTypes(AppDbContext context) + { + var folder = HttpContext.Current.Server.MapPath("~/App_Data"); + var inventoryTypeFile = Path.Combine(folder, "InventoryTypeSeedData.xlsx"); + var parser = new InventoryTypeParser(new FileInfo(inventoryTypeFile)); + foreach (var inventoryType in parser.Parse()) + { + context.InventoryTypes.Add(inventoryType); + } + } + private static void AddInventory(AppDbContext context) { - var peanutButterSmooth = new InventoryType - { - ContainerType = "18 oz jars", - Name = "Peanut Butter Smth 18", - Id = "100395", - UnitsPerCase = 12, - PricePerCase = 14.55M, - WeightPerCase = 13.5 - }; - context.InventoryTypes.Add(peanutButterSmooth); - - var beanInventoryType = new InventoryType - { - ContainerType = "#300 cans", - Name = "Beans, Veg 300", - Id = "100363", - UnitsPerCase = 24, - PricePerCase = 10.18M, - WeightPerCase = 24 - }; - context.InventoryTypes.Add(beanInventoryType); - - var corn = new InventoryType - { - ContainerType = "#300 cans", - Name = "Corn Kernel 300", - Id = "100311", - UnitsPerCase = 24, - PricePerCase = 10.03M, - WeightPerCase = 22.9 - }; - context.InventoryTypes.Add(corn); + var pork = context.InventoryTypes.First(it => it.Identifier == "100139"); + var beans = context.InventoryTypes.First(it => it.Identifier == "100363"); + var pb = context.InventoryTypes.First(it => it.Identifier == "100395"); context.Inventories.Add(new Inventory { - InventoryType = beanInventoryType, + InventoryType = pork, ExpirationDate = DateTime.Now.AddYears(1).AtMidnight(), - Memo = "French cut", + AddedDate = DateTime.Now.AddDays(-1).AtMidnight(), + Memo = "Hormel", Quantity = 10, - Transactions = new List { new Transaction { AddedQuantity = 10, Memo = "arrival", TransactionDate = DateTime.Now} } + Transactions = new List { new Transaction + { + AddedQuantity = 10, Memo = "arrival", TransactionDate = DateTime.Now.AddDays(-1).AtMidnight(), Timestamp = DateTime.Now + }} + }); + + context.Inventories.Add(new Inventory + { + InventoryType = beans, + ExpirationDate = DateTime.Now.AddMonths(4).AtMidnight(), + AddedDate = DateTime.Now.AddMonths(-2).AtMidnight(), + Memo = "Cut", + Quantity = 15, + Transactions = new List { new Transaction + { + AddedQuantity = 15, Memo = "arrival", TransactionDate = DateTime.Now.AddMonths(-2).AtMidnight(), Timestamp = DateTime.Now + }} + }); + + context.Inventories.Add(new Inventory + { + InventoryType = pb, + ExpirationDate = DateTime.Now.AddDays(300).AtMidnight(), + AddedDate = DateTime.Now.AddDays(-34).AtMidnight(), + Quantity = 700, + Transactions = new List { new Transaction + { + AddedQuantity = 700, Memo = "arrival", TransactionDate = DateTime.Now.AddDays(-34).AtMidnight(), Timestamp = DateTime.Now + }} }); } diff --git a/InventoryTraker.Web/Controllers/AuthenticationController.cs b/InventoryTraker.Web/Controllers/AuthenticationController.cs index e33481a..fbd4c5b 100644 --- a/InventoryTraker.Web/Controllers/AuthenticationController.cs +++ b/InventoryTraker.Web/Controllers/AuthenticationController.cs @@ -38,7 +38,7 @@ namespace InventoryTraker.Web.Controllers var identity = _userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); - _authManager.SignIn(new AuthenticationProperties { IsPersistent = false }, identity); + _authManager.SignIn(new AuthenticationProperties { IsPersistent = true }, identity); return Json(true); } diff --git a/InventoryTraker.Web/Controllers/ControllerBase.cs b/InventoryTraker.Web/Controllers/ControllerBase.cs index 5e90f48..3efc08b 100644 --- a/InventoryTraker.Web/Controllers/ControllerBase.cs +++ b/InventoryTraker.Web/Controllers/ControllerBase.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System.Linq; +using System.Web.Mvc; using InventoryTraker.Web.ActionResults; namespace InventoryTraker.Web.Controllers @@ -9,5 +10,16 @@ namespace InventoryTraker.Web.Controllers { return new BetterJsonResult() {Data = model}; } + + protected JsonResult PackageModelStateErrors() + { + var betterJsonResult = new BetterJsonResult(); + foreach (var err in ModelState.Where(ms => ms.Value.Errors.Any())) + { + betterJsonResult.AddError( + err.Key + ": " + string.Join(", ", err.Value.Errors.Select(e => e.ErrorMessage))); + } + return betterJsonResult; + } } } \ No newline at end of file diff --git a/InventoryTraker.Web/Controllers/InventoryController.cs b/InventoryTraker.Web/Controllers/InventoryController.cs index 634f9ee..148fd85 100644 --- a/InventoryTraker.Web/Controllers/InventoryController.cs +++ b/InventoryTraker.Web/Controllers/InventoryController.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Web.Mvc; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -8,25 +10,6 @@ using InventoryTraker.Web.Models; namespace InventoryTraker.Web.Controllers { - public class InventoryTypeController : ControllerBase - { - private readonly AppDbContext _context; - - public InventoryTypeController(AppDbContext context) - { - _context = context; - } - - public JsonResult All() - { - var viewModels = _context.InventoryTypes - .OrderByDescending(x => x.Name) - .ProjectTo(); - - return BetterJson(viewModels.ToArray()); - } - } - public class InventoryController : ControllerBase { private readonly AppDbContext _context; @@ -44,7 +27,7 @@ namespace InventoryTraker.Web.Controllers public JsonResult All() { var viewModels = _context.Inventories - .OrderByDescending(x => x.InventoryType.Name) + .OrderBy(x => x.InventoryType.Name) .ProjectTo() .ToArray(); @@ -53,9 +36,20 @@ namespace InventoryTraker.Web.Controllers public JsonResult Add(InventoryAddForm form) { + if (!ModelState.IsValid) + return PackageModelStateErrors(); + var inventory = Mapper.Map(form); inventory.InventoryType = _context.InventoryTypes.Find(form.InventoryTypeId); _context.Inventories.Add(inventory); + inventory.Transactions = new List(); + inventory.Transactions.Add(new Transaction + { + AddedQuantity = inventory.Quantity, + Memo = "Arrival", + Timestamp = DateTime.Now, + TransactionDate = inventory.AddedDate + }); _context.SaveChanges(); var model = Mapper.Map(inventory); diff --git a/InventoryTraker.Web/Controllers/InventoryTypeController.cs b/InventoryTraker.Web/Controllers/InventoryTypeController.cs new file mode 100644 index 0000000..b13c451 --- /dev/null +++ b/InventoryTraker.Web/Controllers/InventoryTypeController.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Web.Mvc; +using AutoMapper.QueryableExtensions; +using InventoryTraker.Web.Data; +using InventoryTraker.Web.Models; + +namespace InventoryTraker.Web.Controllers +{ + public class InventoryTypeController : ControllerBase + { + private readonly AppDbContext _context; + + public InventoryTypeController(AppDbContext context) + { + _context = context; + } + + public JsonResult All() + { + var viewModels = _context.InventoryTypes + .OrderByDescending(x => x.Name) + .ProjectTo(); + + return BetterJson(viewModels.ToArray()); + } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web/Core/Inventory.cs b/InventoryTraker.Web/Core/Inventory.cs index ad287fc..d1d553a 100644 --- a/InventoryTraker.Web/Core/Inventory.cs +++ b/InventoryTraker.Web/Core/Inventory.cs @@ -1,61 +1,27 @@ using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace InventoryTraker.Web.Core { // AKA Commodity - public class InventoryType - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public string Id { get; set; } - - [Required] - public string Name { get; set; } - - [Required] - public int UnitsPerCase { get; set; } - - [Required] - public string ContainerType { get; set; } - - public double WeightPerCase { get; set; } - - public decimal PricePerCase { get; set; } - } - public class Inventory { public int Id { get; set; } public virtual InventoryType InventoryType { get; set; } - public virtual ICollection Transactions { get; set; } + public virtual ICollection Transactions { get; set; } [Required] public DateTime ExpirationDate { get; set; } + [Required] + public DateTime AddedDate { get; set; } + [Required] public int Quantity { get; set; } public string Memo { get; set; } } - - public class Transaction - { - public int Id { get; set; } - - public virtual Inventory Inventory { get; set; } - - public int AddedQuantity { get; set; } - - public int RemovedQuantity { get; set; } - - public DateTime TransactionDate { get; set; } - - public string Memo { get; set; } - - } } \ No newline at end of file diff --git a/InventoryTraker.Web/Core/InventoryType.cs b/InventoryTraker.Web/Core/InventoryType.cs new file mode 100644 index 0000000..37965a8 --- /dev/null +++ b/InventoryTraker.Web/Core/InventoryType.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace InventoryTraker.Web.Core +{ + public class InventoryType + { + public int Id { get; set; } + + [Required] + public string Identifier { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public int UnitsPerCase { get; set; } + + [Required] + public string ContainerType { get; set; } + + public double WeightPerCase { get; set; } + + public decimal PricePerCase { get; set; } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web/Core/Transaction.cs b/InventoryTraker.Web/Core/Transaction.cs new file mode 100644 index 0000000..5814e7a --- /dev/null +++ b/InventoryTraker.Web/Core/Transaction.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace InventoryTraker.Web.Core +{ + public class Transaction + { + public int Id { get; set; } + + public virtual Inventory Inventory { get; set; } + + [Required] + public int AddedQuantity { get; set; } + + [Required] + public int RemovedQuantity { get; set; } + + [Required] + public DateTime TransactionDate { get; set; } + + public string Memo { get; set; } + + [Required] + public DateTime Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web/Helpers/AngularModelHelper.cs b/InventoryTraker.Web/Helpers/AngularModelHelper.cs index adc8c08..434a237 100644 --- a/InventoryTraker.Web/Helpers/AngularModelHelper.cs +++ b/InventoryTraker.Web/Helpers/AngularModelHelper.cs @@ -35,9 +35,12 @@ namespace InventoryTraker.Web.Helpers /// Converts a lambda expression into a camel-cased AngularJS binding expression, ie: /// {{vm.model.parentProperty.childProperty}} /// - public IHtmlString BindingFor(Expression> property) + public IHtmlString BindingFor(Expression> property, string filter = "") { - return MvcHtmlString.Create("{{" + ExpressionForInternal(property) + "}}"); + return MvcHtmlString.Create("{{" + + ExpressionForInternal(property) + + (!string.IsNullOrEmpty(filter) ? " | " + filter : string.Empty) + + "}}"); } /// @@ -120,7 +123,7 @@ namespace InventoryTraker.Web.Helpers { //input.Attr("type", "date"); input.Attr("bs-datepicker"); - input.Attr("data-date-format", "dd/MM/yyyy"); + input.Attr("data-date-format", "d/M/yyyy"); } if (metadata.DataTypeName == "PhoneNumber") diff --git a/InventoryTraker.Web/InventoryTraker.Web.csproj b/InventoryTraker.Web/InventoryTraker.Web.csproj index dd1229c..8d19e03 100644 --- a/InventoryTraker.Web/InventoryTraker.Web.csproj +++ b/InventoryTraker.Web/InventoryTraker.Web.csproj @@ -47,6 +47,22 @@ ..\packages\AutoMapper.4.2.0\lib\net45\AutoMapper.dll True + + ..\packages\ClosedXML.0.76.0\lib\net40-client\ClosedXML.dll + True + + + ..\packages\CsvHelper.2.11.0\lib\net40-client\CsvHelper.dll + True + + + ..\packages\CsvHelper.Excel.1.0.5\lib\net40-client\CsvHelper.Excel.dll + True + + + ..\packages\DocumentFormat.OpenXml.2.5\lib\DocumentFormat.OpenXml.dll + True + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll True @@ -211,6 +227,7 @@ + @@ -234,7 +251,6 @@ - @@ -263,6 +279,10 @@ + + PreserveNewest + + @@ -289,6 +309,7 @@ + @@ -296,8 +317,10 @@ + + @@ -329,6 +352,8 @@ + + @@ -343,17 +368,17 @@ - - - + + + - - - - + + + + - + Web.config @@ -363,7 +388,6 @@ - diff --git a/InventoryTraker.Web/Models/InventoryAddForm.cs b/InventoryTraker.Web/Models/InventoryAddForm.cs index 0d445f8..8205d5d 100644 --- a/InventoryTraker.Web/Models/InventoryAddForm.cs +++ b/InventoryTraker.Web/Models/InventoryAddForm.cs @@ -9,12 +9,13 @@ namespace InventoryTraker.Web.Models public class InventoryAddForm : IMapTo { [HiddenInput(DisplayValue = false)] - public string InventoryTypeId { get; set; } + [Required] + public int InventoryTypeId { get; set; } [Required] public DateTime ExpirationDate { get; set; } - [Required] + [Required, Range(1, int.MaxValue, ErrorMessage = "Quantity must be greater than 0")] public int Quantity { get; set; } [Required, Display(Name = "Arrival Date")] diff --git a/InventoryTraker.Web/Models/InventoryTypeViewModel.cs b/InventoryTraker.Web/Models/InventoryTypeViewModel.cs index 2f8c7ae..da8f12b 100644 --- a/InventoryTraker.Web/Models/InventoryTypeViewModel.cs +++ b/InventoryTraker.Web/Models/InventoryTypeViewModel.cs @@ -5,7 +5,9 @@ namespace InventoryTraker.Web.Models { public class InventoryTypeViewModel : IMapFrom { - public string Id { get; set; } + public int Id { get; set; } + + public string Identifier { get; set; } public string Name { get; set; } diff --git a/InventoryTraker.Web/Models/InventoryViewModel.cs b/InventoryTraker.Web/Models/InventoryViewModel.cs index d8ca5a5..c22dabe 100644 --- a/InventoryTraker.Web/Models/InventoryViewModel.cs +++ b/InventoryTraker.Web/Models/InventoryViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AutoMapper; using Heroic.AutoMapper; using InventoryTraker.Web.Core; @@ -23,13 +24,19 @@ namespace InventoryTraker.Web.Models public DateTime ExpirationDate { get; set; } + public DateTime AddedDate { get; set; } + + public string Memo { get; set; } + public void CreateMappings(IMapperConfiguration configuration) { configuration.CreateMap() .ForMember(d => d.Name, opt => opt.MapFrom(s => s.InventoryType.Name)) .ForMember(d => d.UnitsPerCase, opt => opt.MapFrom(s => s.InventoryType.UnitsPerCase)) + .ForMember(d => d.ContainerType, opt => opt.MapFrom(s => s.InventoryType.ContainerType)) .ForMember(d => d.WeightPerCase, opt => opt.MapFrom(s => s.InventoryType.WeightPerCase)) - .ForMember(d => d.PricePerCase, opt => opt.MapFrom(s => s.InventoryType.PricePerCase)); + .ForMember(d => d.PricePerCase, opt => opt.MapFrom(s => s.InventoryType.PricePerCase)) + .ForMember(d => d.AddedDate, opt => opt.MapFrom(s => s.AddedDate)); } } } \ No newline at end of file diff --git a/InventoryTraker.Web/Properties/PublishProfiles/ETHRA.pubxml b/InventoryTraker.Web/Properties/PublishProfiles/ETHRA.pubxml new file mode 100644 index 0000000..530d421 --- /dev/null +++ b/InventoryTraker.Web/Properties/PublishProfiles/ETHRA.pubxml @@ -0,0 +1,33 @@ + + + + + Package + Release + Any CPU + + True + False + C:\Users\poprhythm\Documents\code\PublishPackages\InventoryTraker.Web.zip + true + Default Web Site + + + + + + + + + + + + + + Data Source=localhost;Initial Catalog=InventoryTraker;User Id=InventoryTrakerUser;Password=QcXxvpztGp1;Connect Timeout=60 + + + \ No newline at end of file diff --git a/InventoryTraker.Web/Utilities/ExcelParserBase.cs b/InventoryTraker.Web/Utilities/ExcelParserBase.cs new file mode 100644 index 0000000..d534af7 --- /dev/null +++ b/InventoryTraker.Web/Utilities/ExcelParserBase.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using CsvHelper; +using CsvHelper.Configuration; +using CsvHelper.Excel; + +namespace InventoryTraker.Web.Utilities +{ + public class ExcelParserBase : IDisposable + { + protected readonly CsvReader CsvReader; + + protected ExcelParserBase(FileSystemInfo excelFile, CsvConfiguration csvConfiguration) + { + CsvReader = OpenExcel(excelFile, csvConfiguration); + } + + protected ExcelParserBase(FileSystemInfo excelFile) : + this(excelFile, + new CsvConfiguration + { + HasHeaderRecord = false, + IgnoreBlankLines = false, + IgnoreReadingExceptions = true + }) + { + } + + internal static CsvReader OpenExcel(FileSystemInfo excelFile, CsvConfiguration csvConfiguration = null) + { + if (!excelFile.Exists) + throw new FileNotFoundException($"Cannot find file '{excelFile.Name}'"); + + var excelParser = + csvConfiguration != null + ? new ExcelParser(excelFile.FullName, csvConfiguration) + : new ExcelParser(excelFile.FullName); + return new CsvReader(excelParser); + } + + protected string[] GetNextCsvRowValues() + { + // get values from row + if (!CsvReader.Read()) + return null; + + return CsvReader.CurrentRecord; + } + + public void Dispose() + { + CsvReader.Dispose(); + } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web/Utilities/InventoryTypeParser.cs b/InventoryTraker.Web/Utilities/InventoryTypeParser.cs new file mode 100644 index 0000000..d20d586 --- /dev/null +++ b/InventoryTraker.Web/Utilities/InventoryTypeParser.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ClosedXML.Excel; +using CsvHelper.Configuration; +using InventoryTraker.Web.Core; + +namespace InventoryTraker.Web.Utilities +{ + public class InventoryTypeParser + { + private readonly FileSystemInfo _excelFile; + + private sealed class InventoryTypeMap : CsvClassMap + { + public InventoryTypeMap() + { + Map(m => m.Identifier); + Map(m => m.Name); + Map(m => m.UnitsPerCase); + Map(m => m.ContainerType); + Map(m => m.WeightPerCase); + Map(m => m.PricePerCase); + } + } + + public InventoryTypeParser(FileSystemInfo excelFile) + { + _excelFile = excelFile; + } + + public IList Parse() + { + var csvConfiguration = + new CsvConfiguration { IsHeaderCaseSensitive = false, IgnoreReadingExceptions = true}; + csvConfiguration.RegisterClassMap(); + using (var reader = ExcelParserBase.OpenExcel(_excelFile, csvConfiguration)) + { + return reader.GetRecords().ToList(); + } + } + } +} \ No newline at end of file diff --git a/InventoryTraker.Web/Views/Inventory/Index.cshtml b/InventoryTraker.Web/Views/Inventory/Index.cshtml index 3ab8458..9a3481f 100644 --- a/InventoryTraker.Web/Views/Inventory/Index.cshtml +++ b/InventoryTraker.Web/Views/Inventory/Index.cshtml @@ -9,7 +9,5 @@ Inventory -
- -
+ \ No newline at end of file diff --git a/InventoryTraker.Web/Views/Shared/_Layout.cshtml b/InventoryTraker.Web/Views/Shared/_Layout.cshtml index f164e1b..7d6cd20 100644 --- a/InventoryTraker.Web/Views/Shared/_Layout.cshtml +++ b/InventoryTraker.Web/Views/Shared/_Layout.cshtml @@ -6,7 +6,7 @@ @ViewBag.Title - InventoryTraker - @Styles.Render("~/Content/all.css") + @Styles.Render("~/Content/all-styles") @RenderSection("Styles", required: false) @@ -17,7 +17,7 @@
- @(Request.IsAuthenticated ? Html.Partial("_NavigationHero") : Html.Partial("_NavigationNoAuth")) + @(Request.IsAuthenticated ? Html.Partial("_Navigation") : Html.Partial("_NavigationNoAuth"))
@RenderBody() @@ -26,7 +26,7 @@
- @Scripts.Render("~/js/all.js") + @Scripts.Render("~/js/all-javascript") @RenderSection("Scripts", required: false) diff --git a/InventoryTraker.Web/js/inventory/InventoryAddDirective.js b/InventoryTraker.Web/js/inventory/InventoryAddDirective.js index 5680433..6124137 100644 --- a/InventoryTraker.Web/js/inventory/InventoryAddDirective.js +++ b/InventoryTraker.Web/js/inventory/InventoryAddDirective.js @@ -14,18 +14,25 @@ controller.$inject = ['$scope', 'inventorySvc', 'inventoryTypeSvc']; function controller($scope, inventorySvc, inventoryTypeSvc) { var vm = this; - vm.add = add; + vm.add = add; vm.saving = false; vm.inventory = {}; vm.inventoryTypes = inventoryTypeSvc.inventoryTypes; vm.errorMessage = null; vm.quantity = quantity; + function zeroNaN(v) { + return isNaN(v) ? 0 : v; + } + function quantity() { vm.inventory.quantity = - $scope.palletCount * $scope.casesPerPallet + $scope.caseCount; - return vm.inventory.quantity; + zeroNaN($scope.palletCount) + * zeroNaN($scope.casesPerPallet) + + zeroNaN($scope.caseCount); + + return vm.inventory.quantity > 0 ? vm.inventory.quantity : ""; } function add() { @@ -36,14 +43,15 @@ $scope.$close(); }) .error(function(data) { - vm.errorMessage = 'There was a problem adding the inventory: ' + data; + vm.errorMessage = + 'There was a problem adding the inventory: ' + data.errorMessage; }) .finally(function() { vm.saving = false; }); } - $scope.$watch('commodity', function (newValue, oldValue) { + $scope.$watch('vm.commodity', function (newValue, oldValue) { if (newValue) vm.inventory.inventoryTypeId = newValue.id; }); diff --git a/InventoryTraker.Web/js/inventory/InventoryDetailsDirective.js b/InventoryTraker.Web/js/inventory/InventoryDetailsDirective.js index 05a161e..784939a 100644 --- a/InventoryTraker.Web/js/inventory/InventoryDetailsDirective.js +++ b/InventoryTraker.Web/js/inventory/InventoryDetailsDirective.js @@ -4,9 +4,7 @@ window.app.directive('inventoryDetails', inventoryDetails); function inventoryDetails() { return { - scope: { - inventory: '=' - }, + scope: {"inventories" : "="}, templateUrl: '/inventory/template/inventoryDetails.tmpl.cshtml', controller: controller, controllerAs: 'vm' @@ -17,9 +15,9 @@ function controller($scope, $uibModal) { var vm = this; - vm.inventory = $scope.inventory; + vm.inventories = $scope.inventories; vm.edit = edit; - + function edit() { $uibModal.open({ template: '', diff --git a/InventoryTraker.Web/js/inventory/InventoryListController.js b/InventoryTraker.Web/js/inventory/InventoryListController.js index 157e4d2..7f72fe9 100644 --- a/InventoryTraker.Web/js/inventory/InventoryListController.js +++ b/InventoryTraker.Web/js/inventory/InventoryListController.js @@ -11,7 +11,8 @@ function add() { $uibModal.open({ - template: '' + template: '', + backdrop: 'static' }); } } diff --git a/InventoryTraker.Web/js/inventory/inventorySvc.js b/InventoryTraker.Web/js/inventory/inventorySvc.js index ba9cc33..55ae0b1 100644 --- a/InventoryTraker.Web/js/inventory/inventorySvc.js +++ b/InventoryTraker.Web/js/inventory/inventorySvc.js @@ -42,56 +42,6 @@ if (inventories[i].Id == id) return inventories[i]; } - return null; - } - } -})(); - - -(function () { - window.app.factory('inventoryTypeSvc', inventoryTypeSvc); - - inventoryTypeSvc.$inject = ['$http']; - function inventoryTypeSvc($http) { - var inventoryTypes = []; - - loadInventoryTypes(); - - var svc = { - add: add, - update: update, - inventoryTypes: inventoryTypes, - getInventoryType: getInventoryType - }; - - return svc; - - function loadInventoryTypes() { - $http.post('/InventoryType/All') - .success(function (data) { - inventoryTypes.addRange(data); - }); - } - - function add(inventoryType) { - return $http.post('/InventoryType/Add', inventoryType) - .success(function (inventoryType) { - inventoryTypes.unshift(inventoryType); - }); - } - - function update(existingInventory, updatedInventory) { - return $http.post('/InventoryType/Update', updatedInventory) - .success(function (inventory) { - angular.extend(existingInventory, inventory); - }); - } - - function getInventoryType(id) { - for (var i = 0; i < inventoryTypes.length; i++) { - if (inventoryTypes[i].Id == id) return inventoryTypes[i]; - } - return null; } } diff --git a/InventoryTraker.Web/js/inventory/inventoryTypeSvc.js b/InventoryTraker.Web/js/inventory/inventoryTypeSvc.js new file mode 100644 index 0000000..5e30bd5 --- /dev/null +++ b/InventoryTraker.Web/js/inventory/inventoryTypeSvc.js @@ -0,0 +1,48 @@ +(function () { + window.app.factory('inventoryTypeSvc', inventoryTypeSvc); + + inventoryTypeSvc.$inject = ['$http']; + function inventoryTypeSvc($http) { + var inventoryTypes = []; + + loadInventoryTypes(); + + var svc = { + add: add, + update: update, + inventoryTypes: inventoryTypes, + getInventoryType: getInventoryType + }; + + return svc; + + function loadInventoryTypes() { + $http.post('/InventoryType/All') + .success(function (data) { + inventoryTypes.addRange(data); + }); + } + + function add(inventoryType) { + return $http.post('/InventoryType/Add', inventoryType) + .success(function (inventoryType) { + inventoryTypes.unshift(inventoryType); + }); + } + + function update(existingInventory, updatedInventory) { + return $http.post('/InventoryType/Update', updatedInventory) + .success(function (inventory) { + angular.extend(existingInventory, inventory); + }); + } + + function getInventoryType(id) { + for (var i = 0; i < inventoryTypes.length; i++) { + if (inventoryTypes[i].Id == id) return inventoryTypes[i]; + } + + return null; + } + } +})(); \ No newline at end of file diff --git a/InventoryTraker.Web/js/inventory/templates/inventoryAdd.tmpl.cshtml b/InventoryTraker.Web/js/inventory/templates/inventoryAdd.tmpl.cshtml index 0f6f58b..8dfe205 100644 --- a/InventoryTraker.Web/js/inventory/templates/inventoryAdd.tmpl.cshtml +++ b/InventoryTraker.Web/js/inventory/templates/inventoryAdd.tmpl.cshtml @@ -30,22 +30,22 @@
-
-
Selected Commodity
+
Commodity ID
-
{{commodity.id}}
-
Name
-
{{commodity.name}}
+
{{vm.commodity.identifier}}
Units per Case
-
{{commodity.unitsPerCase}} / {{commodity.containerType}}
+
{{vm.commodity.unitsPerCase}} / {{vm.commodity.containerType}}
@@ -57,28 +57,28 @@
-
Pallets
+
- +
-
Cases per Pallet
+
- +

-
Individual Cases
+
- +

-
Total Units
-
{{vm.quantity()}}
+
Total Units
+
{{vm.quantity()}}
diff --git a/InventoryTraker.Web/js/inventory/templates/inventoryDetails.tmpl.cshtml b/InventoryTraker.Web/js/inventory/templates/inventoryDetails.tmpl.cshtml index d5589c1..4ca79b3 100644 --- a/InventoryTraker.Web/js/inventory/templates/inventoryDetails.tmpl.cshtml +++ b/InventoryTraker.Web/js/inventory/templates/inventoryDetails.tmpl.cshtml @@ -1,12 +1,30 @@ @using InventoryTraker.Web.Helpers @model InventoryTraker.Web.Models.InventoryViewModel @{ - var inventory = Html.Angular().ModelFor("vm.inventory"); + var inventory = Html.Angular().ModelFor("inventory"); } -
-
- @inventory.BindingFor(x => x.Name) -
- Quantity - @inventory.BindingFor(x => x.Quantity) -
+ + + + + + + + + + + + + + + + + + + + + + + + +
NameUnits per CaseCase QtyUnit QtyExp DateAdded DateMemo
@inventory.BindingFor(x => x.Name) @inventory.BindingFor(x => x.UnitsPerCase) / @inventory.BindingFor(x => x.ContainerType)@inventory.BindingFor(x => x.Quantity){{inventory.quantity * inventory.unitsPerCase}}@inventory.BindingFor(x => x.ExpirationDate, "date:'shortDate'")@inventory.BindingFor(x => x.AddedDate, "date:'shortDate'")@inventory.BindingFor(x => x.Memo)
diff --git a/InventoryTraker.Web/packages.config b/InventoryTraker.Web/packages.config index cabeafa..053c902 100644 --- a/InventoryTraker.Web/packages.config +++ b/InventoryTraker.Web/packages.config @@ -8,6 +8,10 @@ + + + + diff --git a/InventoryTraker.sln b/InventoryTraker.sln index 2f7d52a..caa6a9d 100644 --- a/InventoryTraker.sln +++ b/InventoryTraker.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InventoryTraker.Web", "InventoryTraker.Web\InventoryTraker.Web.csproj", "{5E5867A4-6152-4655-A04B-1737DF493A41}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InventoryTraker.Web.Tests", "InventoryTraker.Web.Tests\InventoryTraker.Web.Tests.csproj", "{03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {5E5867A4-6152-4655-A04B-1737DF493A41}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E5867A4-6152-4655-A04B-1737DF493A41}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E5867A4-6152-4655-A04B-1737DF493A41}.Release|Any CPU.Build.0 = Release|Any CPU + {03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B3BDF2-B2BE-42C1-8D6F-2B4E106A1D4B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/global.json b/global.json new file mode 100644 index 0000000..3eb265a --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "projects": [ + "wrap" + ] +} \ No newline at end of file diff --git a/wrap/InventoryTraker.Web/project.json b/wrap/InventoryTraker.Web/project.json new file mode 100644 index 0000000..67cd84a --- /dev/null +++ b/wrap/InventoryTraker.Web/project.json @@ -0,0 +1,52 @@ +{ + "version": "1.0.0-*", + "frameworks": { + "net451": { + "wrappedProject": "../../InventoryTraker.Web/InventoryTraker.Web.csproj", + "bin": { + "assembly": "../../InventoryTraker.Web/obj/{configuration}/InventoryTraker.Web.dll", + "pdb": "../../InventoryTraker.Web/obj/{configuration}/InventoryTraker.Web.pdb" + }, + "dependencies": { + "Angular.UI.Bootstrap": "1.3.3", + "AngularJS.Animate": "1.5.8", + "AngularJS.Core": "1.5.8", + "angular-strap": "2.3.1", + "angular-ui-grid": "3.1.1", + "Antlr": "3.5.0.2", + "AutoMapper": "4.2.0", + "bootstrap": "3.3.7", + "EntityFramework": "6.1.3", + "FontAwesome": "4.6.3", + "Heroic.AutoMapper": "2.0.0", + "Heroic.Web.IoC": "4.1.2", + "HtmlTags": "3.0.0.186", + "Humanizer": "1.33.7", + "jQuery": "1.9.1", + "Microsoft.AspNet.Identity.Core": "2.2.1", + "Microsoft.AspNet.Identity.EntityFramework": "2.2.1", + "Microsoft.AspNet.Identity.Owin": "2.2.1", + "Microsoft.AspNet.Mvc": "5.2.3", + "Microsoft.AspNet.Mvc.Futures": "5.0.0", + "Microsoft.AspNet.Razor": "3.2.3", + "Microsoft.AspNet.Web.Optimization": "1.1.3", + "Microsoft.AspNet.WebApi": "5.2.3", + "Microsoft.AspNet.WebApi.Client": "5.2.3", + "Microsoft.AspNet.WebApi.Core": "5.2.3", + "Microsoft.AspNet.WebApi.WebHost": "5.2.3", + "Microsoft.AspNet.WebPages": "3.2.3", + "Microsoft.Owin": "3.0.1", + "Microsoft.Owin.Host.SystemWeb": "3.0.1", + "Microsoft.Owin.Security": "3.0.1", + "Microsoft.Owin.Security.Cookies": "3.0.1", + "Microsoft.Owin.Security.OAuth": "3.0.1", + "Microsoft.Web.Infrastructure": "1.0.0.0", + "Newtonsoft.Json": "6.0.7", + "Owin": "1.0", + "StructureMap": "4.4.0", + "WebActivatorEx": "2.1.0", + "WebGrease": "1.6.0" + } + } + } +} \ No newline at end of file