13 Commits

59 changed files with 983 additions and 922 deletions

View File

@@ -4,21 +4,22 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YAPBusinessLogic.OfficePackage;
using YAPContracts.BusinessLogicContracts;
using YAPContracts.StorageContracts;
using YAPDatabase.Implementations;
namespace YAPBusinessLogic.Implementations
{
internal class ReportBusinessLogicContract
internal class ReportBusinessLogicContract : IReportBusinessLogicContract
{
private readonly ProductStorageContract _productStorageContract;
private readonly PurchaseStorageContract _purchaseStorageContract;
private readonly ProductSetStorageContract _productSetStorageContract;
private readonly ComponentStorageContract _componentStorageContract;
private readonly IProductStorageContract _productStorageContract;
private readonly IPurchaseStorageContract _purchaseStorageContract;
private readonly IProductSetStorageContract _productSetStorageContract;
private readonly IComponentStorageContract _componentStorageContract;
private readonly BasePdfBuilder _pdfBuilder;
public ReportBusinessLogicContract(ProductStorageContract productStorageContract, PurchaseStorageContract purchaseStorageContract, ProductSetStorageContract productSetStorageContract, ComponentStorageContract componentStorageContract, BasePdfBuilder pdfBuilder)
public ReportBusinessLogicContract(IProductStorageContract productStorageContract, IPurchaseStorageContract purchaseStorageContract, IProductSetStorageContract productSetStorageContract, IComponentStorageContract componentStorageContract, BasePdfBuilder pdfBuilder)
{
_productStorageContract = productStorageContract;
_purchaseStorageContract = purchaseStorageContract;
@@ -32,8 +33,7 @@ namespace YAPBusinessLogic.Implementations
/// </summary>
/// <param name="setsIds">Id соборок</param>
/// <param name="format">doc/xls</param>
/// <param name="outputPath">Пусть, куда сохранить файл</param>
public void MakeReportProductsByProductSets(List<string> setsIds, string format, string outputPath)
public Stream MakeReportProductsByProductSets(List<string> setsIds, string format)
{
// Получаем продукты по сборкам
var products = _productStorageContract.GetListProductsByProductSet(setsIds);
@@ -41,44 +41,45 @@ namespace YAPBusinessLogic.Implementations
if (string.Equals(format, "xls", StringComparison.OrdinalIgnoreCase))
{
var excel = new OpenXmlExcelBuilder();
// Заголовок
excel.AddHeader("Изделия - Сборки", 0, 4);
// контент: изделие и под ним сборки
excel.AddHeader("Products by Sets", 0, 4);
// таблица
var rows = new List<string[]>();
rows.Add(new[] { "Product", "Included in Sets" });
foreach (var prod in products)
{
excel.AddParagraph(prod.Product.Name, 0);
foreach (var wm in prod.Sets)
excel.AddParagraph($" {wm.SetName}", 1);
var setNames = prod.Sets.Any()
? string.Join(", ", prod.Sets.Select(s => s.SetName))
: "No Sets.";
rows.Add(new[] { prod.Product?.Name ?? "-", setNames });
}
using var st = excel.Build();
File.WriteAllBytes(outputPath, ((MemoryStream)st).ToArray());
excel.AddTable([10, 10], rows);
return excel.Build();
}
else if (string.Equals(format, "doc", StringComparison.OrdinalIgnoreCase))
{
StringBuilder sb = new StringBuilder();
var word = new OpenXmlWordBuilder();
word.AddHeader("Products by Sets");
// Добавляем заголовок
word.AddHeader("Изделия - Сборки");
// Добавляем контент: название изделия + список сборок
foreach (var prod in products)
{
word.AddParagraph(prod.Product.Name);
foreach (var wm in prod.Sets)
{
word.AddParagraph($" {wm.SetName}");
}
word.AddParagraph($"Product: {prod.Product?.Name}");
sb.Append("Sets: ");
var setNames = prod.Sets.Any()
? string.Join(", ", prod.Sets.Select(s => s.SetName))
: "No Sets.";
sb.Append(setNames);
word.AddParagraph(sb.ToString());
sb.Clear();
}
// Генерируем и сохраняем
using var stream = word.Build();
// Убедимся, что позиция в потоке в начале
if (stream.CanSeek) stream.Seek(0, SeekOrigin.Begin);
File.WriteAllBytes(outputPath, ((MemoryStream)stream).ToArray());
return word.Build();
}
else
{
throw new ArgumentException("Поддерживаются только форматы «doc» и «xls»", nameof(format));
throw new ArgumentException("Only «doc» and «xls» formats are supported", nameof(format));
}
}
@@ -87,8 +88,7 @@ namespace YAPBusinessLogic.Implementations
/// </summary>
/// <param name="productIds">Id товаров</param>
/// <param name="format">doc/xls</param>
/// <param name="outputPath">Пусть, куда сохранить файл</param>
public void MakeReportProductSetsByProducts(List<string> productIds, string format, string outputPath)
public Stream MakeReportProductSetsByProducts(List<string> productIds, string format)
{
// Получаем продукты по сборкам
var productSets = _productSetStorageContract.GetListProductSetsByProducts(productIds);
@@ -96,44 +96,45 @@ namespace YAPBusinessLogic.Implementations
if (string.Equals(format, "xls", StringComparison.OrdinalIgnoreCase))
{
var excel = new OpenXmlExcelBuilder();
// Заголовок
excel.AddHeader("Сборки - Изделия", 0, 4);
// контент: Сборки и под ними изделия
foreach (var prod in productSets)
excel.AddHeader("Product Sets by Products", 0, 4);
var rows = new List<string[]>();
rows.Add(new[] { "Product Set", "Products" });
foreach (var set in productSets)
{
excel.AddParagraph(prod.Set.SetName, 0);
foreach (var wm in prod.Products)
excel.AddParagraph($" {wm.Name}", 1);
var productNames = set.Products.Any()
? string.Join(", ", set.Products.Select(p => p.Name))
: "Нет товаров";
rows.Add(new[] { set.Set?.SetName ?? "-", productNames });
}
using var st = excel.Build();
File.WriteAllBytes(outputPath, ((MemoryStream)st).ToArray());
excel.AddTable([10, 10], rows);
return excel.Build();
}
else if (string.Equals(format, "doc", StringComparison.OrdinalIgnoreCase))
{
StringBuilder sb = new StringBuilder();
var word = new OpenXmlWordBuilder();
word.AddHeader("Product Sets by Products");
// Добавляем заголовок
word.AddHeader("Изделия - Сборки");
// Добавляем контент: название изделия + список сборок
foreach (var prod in productSets)
foreach (var set in productSets)
{
word.AddParagraph(prod.Set.SetName);
foreach (var wm in prod.Products)
{
word.AddParagraph($" {wm.Name}");
}
word.AddParagraph($"Сборка: {set.Set?.SetName}");
sb.Append("Товары: ");
var productNames = set.Products.Any()
? string.Join(", ", set.Products.Select(p => p.Name))
: "Нет товаров";
sb.Append(productNames);
word.AddParagraph(sb.ToString());
sb.Clear();
}
// Генерируем и сохраняем
using var stream = word.Build();
// Убедимся, что позиция в потоке в начале
if (stream.CanSeek) stream.Seek(0, SeekOrigin.Begin);
File.WriteAllBytes(outputPath, ((MemoryStream)stream).ToArray());
return word.Build();
}
else
{
throw new ArgumentException("Поддерживаются только форматы «doc» и «xls»", nameof(format));
throw new ArgumentException("Only «doc» and «xls» formats are supported", nameof(format));
}
}
@@ -144,40 +145,40 @@ namespace YAPBusinessLogic.Implementations
/// <param name="dateEnd">Конец периода</param>
public Stream MakeReportByPurchases(DateTime dateStart, DateTime dateEnd)
{
_pdfBuilder.AddHeader("Отчет по продажам");
_pdfBuilder.AddHeader("Purchases report");
var reportData = _purchaseStorageContract.GetDataForReport(dateStart, dateEnd);
_pdfBuilder.AddParagraph($"Период: {dateStart} — {dateEnd}");
_pdfBuilder.AddParagraph($"Period: {dateStart.ToLocalTime()} — {dateEnd.ToLocalTime()}");
foreach (var sale in reportData)
{
_pdfBuilder.AddParagraph($"----------------------------------");
_pdfBuilder.AddParagraph($"Дата продажи: {sale.Purchase.PurchaseDate}");
_pdfBuilder.AddParagraph($"Сумма: {sale.Purchase.TotalPrice}");
_pdfBuilder.AddParagraph($"Purchase date: {sale.Purchase.PurchaseDate}");
_pdfBuilder.AddParagraph($"Total: {sale.Purchase.TotalPrice}");
_pdfBuilder.AddParagraph($"");
_pdfBuilder.AddParagraph($"Комплектующие");
_pdfBuilder.AddParagraph($"Components");
if (sale.Components is { Count: > 0 })
{
_pdfBuilder.AddTable(
new[] { "Название", "Тип" },
new[] { "Name", "Type" },
sale.Components.Select(p => new[] { p.Name, p.ComponentType.ToString() }).ToList()
);
}
else
{
_pdfBuilder.AddParagraph("Комплектующих не найдено.");
_pdfBuilder.AddParagraph("No components found.");
}
_pdfBuilder.AddParagraph($"Комментарии");
_pdfBuilder.AddParagraph($"Comments");
if (sale.Comments is { Count: > 0 })
{
_pdfBuilder.AddTable(
new[] { "Текст", "Дата" },
new[] { "Text", "Date" },
sale.Comments.Select(p => new[] { p.Text, p.Date.ToString("dd.MM.yyyy") }).ToList()
);
}
else
{
_pdfBuilder.AddParagraph("Комментариев не найдено.");
_pdfBuilder.AddParagraph("No comments found.");
}
}
@@ -192,40 +193,44 @@ namespace YAPBusinessLogic.Implementations
/// <param name="dateEnd">Конец периода</param>
public Stream MakeReportByComponents(DateTime dateStart, DateTime dateEnd)
{
_pdfBuilder.AddHeader("Отчет по комплектующим");
if (dateEnd < dateStart)
{
throw new ArgumentException("End date must be later than start date.");
}
_pdfBuilder.AddHeader("Components report");
var reportData = _componentStorageContract.GetDataForReport(dateStart, dateEnd);
_pdfBuilder.AddParagraph($"Период: {dateStart} — {dateEnd}");
_pdfBuilder.AddParagraph($"Period: {dateStart.ToLocalTime()} — {dateEnd.ToLocalTime()}");
foreach (var comp in reportData)
{
_pdfBuilder.AddParagraph($"----------------------------------");
_pdfBuilder.AddParagraph($"Имя: {comp.Component.Name}");
_pdfBuilder.AddParagraph($"Тип: {comp.Component.ComponentType}");
_pdfBuilder.AddParagraph($"Name: {comp.Component.Name}");
_pdfBuilder.AddParagraph($"Type: {comp.Component.ComponentType}");
_pdfBuilder.AddParagraph($"");
_pdfBuilder.AddParagraph($"Покупки");
_pdfBuilder.AddParagraph($"Purchases");
if (comp.Purchases is { Count: > 0 })
{
_pdfBuilder.AddTable(
new[] { "Дата", "Сумма" },
new[] { "Date", "Sum" },
comp.Purchases.Select(p => new[] { p.PurchaseDate.ToString("MM/dd/yy"), p.TotalPrice.ToString() }).ToList()
);
}
else
{
_pdfBuilder.AddParagraph("Покупок не найдено.");
_pdfBuilder.AddParagraph("No purchases found.");
}
_pdfBuilder.AddParagraph($"Заказы");
_pdfBuilder.AddParagraph($"Orders");
if (comp.Orders is { Count: > 0 })
{
_pdfBuilder.AddTable(
new[] { "Дата", "Имя" },
new[] { "Date", "Dealer Name" },
comp.Orders.Select(p => new[] { p.OrderDate.ToString("dd.MM.yyyy"), p.DealerName }).ToList()
);
}
else
{
_pdfBuilder.AddParagraph("Заказов не найдено.");
_pdfBuilder.AddParagraph("No orders found.");
}
}

View File

@@ -19,5 +19,7 @@ namespace YAPContracts.BindingModels
[Required(ErrorMessage = "This field is required")]
[Display(Name = "Component Type")]
public string? ComponentType { get; set; }
public List<string>? ProductSetIds { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YAPContracts.BusinessLogicContracts
{
public interface IReportBusinessLogicContract
{
Stream MakeReportProductsByProductSets(List<string> setsIds, string format);
Stream MakeReportProductSetsByProducts(List<string> productIds, string format);
public Stream MakeReportByPurchases(DateTime dateStart, DateTime dateEnd);
public Stream MakeReportByComponents(DateTime dateStart, DateTime dateEnd);
}
}

View File

@@ -16,16 +16,17 @@ public class ComponentDataModel : IValidation
public string Name { get; private set; }
public ComponentType ComponentType { get; private set; }
public bool IsDeleted { get; private set; }
// public List<ComponentInProductDataModel> Products = products;
// public List<ComponentInProductSetDataModel> ProductSets = productSets;
public ComponentDataModel(string id, string name, ComponentType componentType, bool isDeleted)
// public List<ComponentInProductDataModel> Products = products;
public List<ComponentInProductSetDataModel> ProductSets { get; private set; }
public ComponentDataModel(string id, string name, ComponentType componentType, bool isDeleted, List<ComponentInProductSetDataModel> productSets)
{
Id = id;
Name = name;
ComponentType = componentType;
IsDeleted = isDeleted;
ProductSets = productSets;
}
public ComponentDataModel()
@@ -34,6 +35,7 @@ public class ComponentDataModel : IValidation
Name = string.Empty;
ComponentType = ComponentType.None;
IsDeleted = false;
ProductSets = new List<ComponentInProductSetDataModel>();
}
public void Validate()

View File

@@ -18,4 +18,5 @@ public interface IComponentStorageContract
void UpdElement(ComponentDataModel componentDataModel);
void DelElement(string id);
List<ComponentReportModel>? GetDataForReport(DateTime start, DateTime finish);
}

View File

@@ -15,5 +15,6 @@ namespace YAPContracts.StorageContracts
void AddElement(ProductSetDataModel productSet);
void UpdateElement(ProductSetDataModel productSet);
void DeleteElement(string id);
List<SetWithProductsDataModel> GetListProductSetsByProducts(List<string> prodIds);
}
}

View File

@@ -17,4 +17,6 @@ public interface IProductStorageContract
void UpdElement(ProductDataModel ProductDataModel);
void DelElement(string id);
List<ProductWithSetsDataModel> GetListProductsByProductSet(List<string> setsIds);
}

View File

@@ -14,5 +14,6 @@ namespace YAPContracts.StorageContracts
void AddElement(PurchaseDataModel purchase);
void UpdateElement(PurchaseDataModel purchase);
void DeleteElement(string id);
List<PurchasesReportModel>? GetDataForReport(DateTime start, DateTime finish);
}
}

View File

@@ -7,4 +7,6 @@ public class ComponentViewModel
public string Id { get; set; } = default!;
public string Name { get; set; } = default!;
public ComponentType ComponentType { get; set; }
public List<string>? ProductSetIds { get; set; }
}

View File

@@ -42,7 +42,8 @@ internal class ComponentStorageContract : IComponentStorageContract
{
try
{
_dbContext.Components.Add(_mapper.Map<Component>(component));
var entity = _mapper.Map<Component>(component);
_dbContext.Components.Add(entity);
_dbContext.SaveChanges();
}
catch (Exception ex)
@@ -89,7 +90,7 @@ internal class ComponentStorageContract : IComponentStorageContract
{
try
{
return _mapper.Map<ComponentDataModel>(_dbContext.Components.FirstOrDefault(x => x.Name == name));
return _mapper.Map<ComponentDataModel>(_dbContext.Components.Include(x => x.ProductSets).FirstOrDefault(x => x.Name == name));
}
catch (Exception ex)
{
@@ -132,7 +133,7 @@ internal class ComponentStorageContract : IComponentStorageContract
}
}
private Component? GetComponentById(string id) => _dbContext.Components.FirstOrDefault(x => x.Id == id);
private Component? GetComponentById(string id) => _dbContext.Components.Include(x => x.ProductSets).FirstOrDefault(x => x.Id == id);
public List<ComponentReportModel>? GetDataForReport(DateTime start, DateTime finish)
{
@@ -143,11 +144,11 @@ internal class ComponentStorageContract : IComponentStorageContract
{
Component = _mapper.Map<ComponentDataModel>(c),
Purchases = c.Products
.SelectMany(cp => cp.Product.ProductsInPurchace)
.SelectMany(cp => cp.Product.ProductsInPurchace).Where(pip => pip.Purchase.PurchaseDate >= start && pip.Purchase.PurchaseDate <= finish)
.Select(pip => _mapper.Map<PurchaseDataModel>(pip.Purchase))
.ToList(),
Orders = c.Products
.SelectMany(p => p.Product.ProductOrders)
.SelectMany(p => p.Product.ProductOrders).Where(po => po.OrderDate >= start && po.OrderDate <= finish)
.Select(ord => _mapper.Map<ProductOrderDataModel>(ord))
.ToList()
})

View File

@@ -166,7 +166,7 @@ namespace YAPDatabase.Implementations
{
try
{
return _dbContext.ProductSets
return _dbContext.ProductSets.Where(x => x.IsDeleted != true)
.Where(p => p.ComponentsInProductSet
.Any(cp => cp.Component.Products
.Any(cps => prodIds.Contains(cps.ProductId))))

View File

@@ -20,8 +20,25 @@ namespace YAPWebApplication.Adapters
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ComponentBindingModel, ComponentDataModel>();
cfg.CreateMap<ComponentDataModel, ComponentViewModel>();
cfg.CreateMap<ComponentBindingModel, ComponentDataModel>()
.ForMember(dest => dest.ProductSets,
opt => opt.MapFrom(src =>
(src.ProductSetIds ?? new List<string>())
.Select(psId => new ComponentInProductSetDataModel(
src.Id ?? Guid.NewGuid().ToString(),
psId,
1
)).ToList()
)
);
cfg.CreateMap<ComponentDataModel, ComponentViewModel>()
.ForMember(dest => dest.ProductSetIds,
opt => opt.MapFrom(src =>
src.ProductSets.Select(ps => ps.ProductSetId).ToList()
)
);
cfg.CreateMap<ComponentViewModel, ComponentDataModel>();
});

View File

@@ -1,99 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class CommentController : Controller
{
private readonly ICommentAdapter _commentAdapter;
public CommentController(ICommentAdapter commentAdapter)
{
_commentAdapter = commentAdapter;
}
// GET: /Comment/
public IActionResult Index()
{
var comments = _commentAdapter.GetList();
return View(comments);
}
// GET: /Comment/Details/{id}
public IActionResult Details(string id)
{
var comment = _commentAdapter.GetCommentById(id);
if (comment == null)
{
return NotFound();
}
return View(comment);
}
// GET: /Comment/Create
public IActionResult Create()
{
return View();
}
// POST: /Comment/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CommentBindingModel model)
{
if (ModelState.IsValid)
{
_commentAdapter.Create(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Comment/Edit/{id}
public IActionResult Edit(string id)
{
var comment = _commentAdapter.GetCommentById(id);
if (comment == null)
{
return NotFound();
}
return View(comment);
}
// POST: /Comment/Edit
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(CommentBindingModel model)
{
if (ModelState.IsValid)
{
_commentAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Comment/Delete/{id}
public IActionResult Delete(string id)
{
var comment = _commentAdapter.GetCommentById(id);
if (comment == null)
{
return NotFound();
}
return View(comment);
}
// POST: /Comment/Delete
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_commentAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,100 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class ComponentController : Controller
{
private readonly IComponentAdapter _componentAdapter;
public ComponentController(IComponentAdapter componentAdapter)
{
_componentAdapter = componentAdapter;
}
// GET: /Component/
public IActionResult Index()
{
var sets = _componentAdapter.GetList(true);
return View(sets);
}
// GET: /Component/Details/{id}
public IActionResult Details(string id)
{
var set = _componentAdapter.GetComponentByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// GET: /Component/Create
public IActionResult Create()
{
return View();
}
// POST: /Component/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(ComponentBindingModel model)
{
if (ModelState.IsValid)
{
_componentAdapter.Insert(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Component/Edit/{id}
public IActionResult Edit(string id)
{
var set = _componentAdapter.GetComponentByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /Component/Edit
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(ComponentBindingModel model)
{
if (ModelState.IsValid)
{
_componentAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Component/Delete/{id}
public IActionResult Delete(string id)
{
var set = _componentAdapter.GetComponentByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /Component/Delete
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_componentAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,99 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class ProductController : Controller
{
private readonly IProductAdapter _productAdapter;
public ProductController(IProductAdapter productAdapter)
{
_productAdapter = productAdapter;
}
// GET: /Product/
public IActionResult Index()
{
var sets = _productAdapter.GetList();
return View(sets);
}
// GET: /Product/Details/{id}
public IActionResult Details(string id)
{
var set = _productAdapter.GetProductByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// GET: /Product/Create
public IActionResult Create()
{
return View();
}
// POST: /Product/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(ProductBindingModel model)
{
if (ModelState.IsValid)
{
_productAdapter.Insert(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Product/Edit/{id}
public IActionResult Edit(string id)
{
var set = _productAdapter.GetProductByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /Product/Edit
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(ProductBindingModel model)
{
if (ModelState.IsValid)
{
_productAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /Product/Delete/{id}
public IActionResult Delete(string id)
{
var set = _productAdapter.GetProductByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /Product/Delete
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_productAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,68 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class ProductOrderController : Controller
{
private readonly IProductOrderAdapter _adapter;
public ProductOrderController(IProductOrderAdapter adapter)
{
_adapter = adapter;
}
// список всех заказов
public IActionResult Index()
{
var orders = _adapter.GetList();
return View(orders);
}
// просмотр деталей
public IActionResult Details(string id)
{
var order = _adapter.GetElement(id);
if (order == null)
{
return NotFound();
}
return View(order);
}
// форма создания
public IActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(ProductOrderBindingModel model)
{
if (ModelState.IsValid)
{
_adapter.RegisterProductOrder(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// заказы за период
public IActionResult ByPeriod(DateTime fromDate, DateTime toDate)
{
var orders = _adapter.GetListByPeriod(fromDate, toDate);
return View("Index", orders); // можно отдельное представление
}
// заказы по продукту за период
public IActionResult ByProductAndPeriod(string productId, DateTime fromDate, DateTime toDate)
{
var orders = _adapter.GetListByProductAndPeriod(productId, fromDate, toDate);
return View("Index", orders);
}
}
}

View File

@@ -1,99 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class ProductSetController : Controller
{
private readonly IProductSetAdapter _productSetAdapter;
public ProductSetController(IProductSetAdapter productSetAdapter)
{
_productSetAdapter = productSetAdapter;
}
// GET: /ProductSet/
public IActionResult Index()
{
var sets = _productSetAdapter.GetList();
return View(sets);
}
// GET: /ProductSet/Details/{id}
public IActionResult Details(string id)
{
var set = _productSetAdapter.GetProductSetByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// GET: /ProductSet/Create
public IActionResult Create()
{
return View();
}
// POST: /ProductSet/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(ProductSetBindingModel model)
{
if (ModelState.IsValid)
{
_productSetAdapter.Insert(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /ProductSet/Edit/{id}
public IActionResult Edit(string id)
{
var set = _productSetAdapter.GetProductSetByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /ProductSet/Edit
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(ProductSetBindingModel model)
{
if (ModelState.IsValid)
{
_productSetAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: /ProductSet/Delete/{id}
public IActionResult Delete(string id)
{
var set = _productSetAdapter.GetProductSetByData(id);
if (set == null)
{
return NotFound();
}
return View(set);
}
// POST: /ProductSet/Delete
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_productSetAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,110 +0,0 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
using YAPContracts.BusinessLogicContracts;
using YAPContracts.DataModels;
using YAPContracts.ViewModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class PurchaseController : Controller
{
private readonly IPurchaseAdapter _purchaseAdapter;
public PurchaseController(IPurchaseAdapter purchaseAdapter)
{
_purchaseAdapter = purchaseAdapter;
}
// список всех покупок
public IActionResult Index()
{
var purchases = _purchaseAdapter.GetList();
return View(purchases);
}
// просмотр деталей
public IActionResult Details(string id)
{
var purchase = _purchaseAdapter.GetElement(id);
if (purchase == null)
{
return NotFound();
}
return View(purchase);
}
// форма добавления
public IActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(PurchaseBindingModel model)
{
if (ModelState.IsValid)
{
_purchaseAdapter.Register(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// форма редактирования
public IActionResult Edit(string id)
{
var purchase = _purchaseAdapter.GetElement(id);
if (purchase == null)
{
return NotFound();
}
var bindingModel = new PurchaseBindingModel
{
Id = purchase.Id,
UserId = purchase.UserId,
PurchaseDate = purchase.PurchaseDate,
TotalPrice = purchase.TotalPrice,
// сюда можно маппить связанные продукты/сборки
};
return View(bindingModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(PurchaseBindingModel model)
{
if (ModelState.IsValid)
{
_purchaseAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// удаление
public IActionResult Delete(string id)
{
var purchase = _purchaseAdapter.GetElement(id);
if (purchase == null)
{
return NotFound();
}
return View(purchase);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_purchaseAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,109 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class StorekeeperController : Controller
{
private readonly IStorekeeperAdapter _storekeeperAdapter;
public StorekeeperController(IStorekeeperAdapter storekeeperAdapter)
{
_storekeeperAdapter = storekeeperAdapter;
}
// список всех работников
public IActionResult Index()
{
var storekeepers = _storekeeperAdapter.GetList();
return View(storekeepers);
}
// просмотр деталей конкретного работника
public IActionResult Details(string id)
{
var storekeeper = _storekeeperAdapter.GetElement(id);
if (storekeeper == null)
{
return NotFound();
}
return View(storekeeper);
}
// GET: форма создания
public IActionResult Create()
{
return View();
}
// POST: создание нового работника
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(StorekeeperBindingModel model)
{
if (ModelState.IsValid)
{
_storekeeperAdapter.Register(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: форма редактирования
public IActionResult Edit(string id)
{
var storekeeper = _storekeeperAdapter.GetElement(id);
if (storekeeper == null)
{
return NotFound();
}
// для редактирования можно заполнить BindingModel из ViewModel
var bindingModel = new StorekeeperBindingModel
{
Id = storekeeper.Id,
Login = storekeeper.Login,
Email = storekeeper.Email,
Password = "", // пароль редактируется отдельно
};
return View(bindingModel);
}
// POST: обновление данных
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(StorekeeperBindingModel model)
{
if (ModelState.IsValid)
{
_storekeeperAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: форма удаления
public IActionResult Delete(string id)
{
var storekeeper = _storekeeperAdapter.GetElement(id);
if (storekeeper == null)
{
return NotFound();
}
return View(storekeeper);
}
// POST: подтверждение удаления
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_storekeeperAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -1,109 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
namespace YAPWebApplication.Controllers
{
[Authorize]
public class WorkerController : Controller
{
private readonly IWorkerAdapter _workerAdapter;
public WorkerController(IWorkerAdapter workerAdapter)
{
_workerAdapter = workerAdapter;
}
// список всех работников
public IActionResult Index()
{
var workers = _workerAdapter.GetList();
return View(workers);
}
// просмотр деталей конкретного работника
public IActionResult Details(string id)
{
var worker = _workerAdapter.GetElement(id);
if (worker == null)
{
return NotFound();
}
return View(worker);
}
// GET: форма создания
public IActionResult Create()
{
return View();
}
// POST: создание нового работника
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(WorkerBindingModel model)
{
if (ModelState.IsValid)
{
_workerAdapter.Register(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: форма редактирования
public IActionResult Edit(string id)
{
var worker = _workerAdapter.GetElement(id);
if (worker == null)
{
return NotFound();
}
// для редактирования можно заполнить BindingModel из ViewModel
var bindingModel = new WorkerBindingModel
{
Id = worker.Id,
Login = worker.Login,
Email = worker.Email,
Password = "", // пароль редактируется отдельно
};
return View(bindingModel);
}
// POST: обновление данных
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(WorkerBindingModel model)
{
if (ModelState.IsValid)
{
_workerAdapter.Update(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// GET: форма удаления
public IActionResult Delete(string id)
{
var worker = _workerAdapter.GetElement(id);
if (worker == null)
{
return NotFound();
}
return View(worker);
}
// POST: подтверждение удаления
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(string id)
{
_workerAdapter.Delete(id);
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -0,0 +1,53 @@
using MailKit.Net.Smtp;
using MimeKit;
namespace YAPWebApplication.Infrastructure
{
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body, Stream? attachment = null, string? fileName = null);
}
public class EmailService : IEmailService
{
private readonly IConfiguration _config;
public EmailService(IConfiguration config)
{
_config = config;
}
public async Task SendEmailAsync(string to, string subject, string body, Stream? attachment = null, string? fileName = null)
{
var emailMessage = new MimeMessage();
emailMessage.From.Add(new MailboxAddress("Shop Reports", _config["Email:SmtpUser"]));
emailMessage.To.Add(MailboxAddress.Parse(to));
emailMessage.Subject = subject;
var builder = new BodyBuilder { HtmlBody = body };
if (attachment != null && fileName != null)
{
attachment.Seek(0, SeekOrigin.Begin);
builder.Attachments.Add(fileName, attachment);
}
emailMessage.Body = builder.ToMessageBody();
using var client = new SmtpClient();
await client.ConnectAsync(_config["Email:SmtpServer"], int.Parse(_config["Email:SmtpPort"]), true);
await client.AuthenticateAsync(_config["Email:SmtpUser"], _config["Email:SmtpPass"]);
try
{
await client.SendAsync(emailMessage);
await client.DisconnectAsync(true);
}
catch (Exception ex)
{
// Log the exception or handle it as needed
throw new InvalidOperationException(ex.Message);
}
}
}
}

View File

@@ -12,11 +12,18 @@
</div>
<div class="list-group">
<a asp-page="/Views/Comments/Index" class="list-group-item list-group-item-action">Comments</a>
<a asp-page="/Views/Components/Index" class="list-group-item list-group-item-action">Components</a>
<a asp-page="/Views/Products/Index" class="list-group-item list-group-item-action">Products</a>
<a asp-page="/Views/ProductSets/Index" class="list-group-item list-group-item-action">Product Sets</a>
<a asp-page="/Views/ProductOrders/Index" class="list-group-item list-group-item-action">Product Orders</a>
<a asp-page="/Views/Purchases/Index" class="list-group-item list-group-item-action">Purchases</a>
<a asp-page="/Reports/Index" class="list-group-item list-group-item-action">Reports</a>
@if (User.IsInRole("Worker")){
<a asp-page="/Views/Comments/Index" class="list-group-item list-group-item-action">Comments</a>
<a asp-page="/Views/ProductSets/Index" class="list-group-item list-group-item-action">Product Sets</a>
<a asp-page="/Views/Purchases/Index" class="list-group-item list-group-item-action">Purchases</a>
}
@if (User.IsInRole("Storekeeper"))
{
<a asp-page="/Views/Components/Index" class="list-group-item list-group-item-action">Components</a>
<a asp-page="/Views/Products/Index" class="list-group-item list-group-item-action">Products</a>
<a asp-page="/Views/ProductOrders/Index" class="list-group-item list-group-item-action">Product Orders</a>
}
<a asp-page="/Views/Reports/Index" class="list-group-item list-group-item-action">Reports</a>
</div>

View File

@@ -3,7 +3,7 @@
@model YAPWebApplication.Pages.Views.Comments.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Comments";
}
<h1>Comments</h1>

View File

@@ -24,6 +24,20 @@
asp-items="Model.ComponentTypeList"></select>
<span asp-validation-for="Component.ComponentType" class="text-danger"></span>
</div>
<div class="form-group">
<label for="ProductSets">Add to Product Set</label>
@foreach (var set in Model.ProductSetsList)
{
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="Component.ProductSetIds"
value="@set.Value"
@(set.Selected ? "checked" : "") />
<label class="form-check-label">@set.Text</label>
</div>
}
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>

View File

@@ -18,20 +18,23 @@ namespace YAPWebApplication.Pages.Views.Components
public class CreateModel : PageModel
{
private readonly IComponentAdapter _componentAdapter;
private readonly IProductSetAdapter _productSetAdapter;
public CreateModel(IComponentAdapter componentAdapter)
public CreateModel(IComponentAdapter componentAdapter, IProductSetAdapter productSetAdapter)
{
_componentAdapter = componentAdapter;
_productSetAdapter = productSetAdapter;
}
public IEnumerable<SelectListItem>? ComponentTypeList { get; set; }
public IEnumerable<SelectListItem>? ProductSetsList { get; set; }
[BindProperty]
public ComponentBindingModel Component { get; set; } = new();
public IActionResult OnGet()
{
LoadComponentTypeList();
LoadLists();
return Page();
}
@@ -39,7 +42,7 @@ namespace YAPWebApplication.Pages.Views.Components
{
if (!ModelState.IsValid)
{
LoadComponentTypeList();
LoadLists();
return Page();
}
@@ -52,12 +55,12 @@ namespace YAPWebApplication.Pages.Views.Components
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Ошибка: {ex.Message}");
LoadComponentTypeList();
LoadLists();
return Page();
}
}
private void LoadComponentTypeList()
private void LoadLists()
{
ComponentTypeList = Enum.GetValues(typeof(ComponentType))
.Cast<ComponentType>()
@@ -66,6 +69,12 @@ namespace YAPWebApplication.Pages.Views.Components
Value = ((int)ct).ToString(),
Text = ct.ToString()
});
var prodSets = _productSetAdapter.GetList();
ProductSetsList = prodSets?.Select(c => new SelectListItem
{
Value = c.Id,
Text = $"{c.SetName}"
}).ToList() ?? new List<SelectListItem>();
}
}
}

View File

@@ -23,6 +23,24 @@
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Component.ComponentType)
</dd>
<dt class="col-sm-2">
Product Sets
</dt>
<dd class="col-sm-10">
@if (Model.ProductSets != null && Model.ProductSets.Any())
{
<ul>
@foreach (var comp in Model.ProductSets)
{
<li>@comp.SetName</li>
}
</ul>
}
else
{
<span>Does not appear in Product Sets</span>
}
</dd>
</dl>
</div>
<div>

View File

@@ -14,25 +14,36 @@ namespace YAPWebApplication.Pages.Views.Components
{
internal class DetailsModel : PageModel
{
private readonly IComponentAdapter _adapter;
private readonly IComponentAdapter _componentAdapter;
private readonly IProductSetAdapter _productSetAdapter;
public DetailsModel(IComponentAdapter adapter)
public DetailsModel(IComponentAdapter adapter, IProductSetAdapter productSetAdapter)
{
_adapter = adapter;
_componentAdapter = adapter;
_productSetAdapter = productSetAdapter;
}
public ComponentViewModel? Component { get; set; }
public List<ProductSetViewModel>? ProductSets { get; set; }
public IActionResult OnGet(string id)
{
if (id == null)
return NotFound();
Component = _adapter.GetComponentByData(id);
Component = _componentAdapter.GetComponentByData(id);
if (Component == null)
return NotFound();
var allSets = _productSetAdapter.GetList();
if (allSets != null && Component.ProductSetIds.Any())
{
ProductSets = allSets
.Where(c => Component.ProductSetIds.Contains(c.Id))
.ToList();
}
return Page();
}
}

View File

@@ -26,6 +26,20 @@
asp-items="Model.ComponentTypeList"></select>
<span asp-validation-for="Component.ComponentType" class="text-danger"></span>
</div>
<div class="form-group">
<label>Product Sets</label>
@foreach (var comp in Model.ProductSetSelectList)
{
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="Component.ProductSetIds"
value="@comp.Value"
@(comp.Selected ? "checked" : "") />
<label class="form-check-label">@comp.Text</label>
</div>
}
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>

View File

@@ -1,30 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
using YAPContracts.Enums;
using YAPDatabase;
using YAPDatabase.Models;
using YAPWebApplication.Adapters;
namespace YAPWebApplication.Pages.Views.Components
{
[Authorize(Roles = "Storekeeper")]
internal class EditModel : PageModel
{
private readonly IComponentAdapter _adapter;
private readonly IComponentAdapter _componentAdapter;
private readonly IProductSetAdapter _productSetAdapter;
public EditModel(IComponentAdapter adapter)
public EditModel(IComponentAdapter adapter, IProductSetAdapter productSetAdapter)
{
_adapter = adapter;
_componentAdapter = adapter;
_productSetAdapter = productSetAdapter;
}
public IEnumerable<SelectListItem>? ComponentTypeList { get; set; }
public List<SelectListItem> ProductSetSelectList { get; set; } = new();
[BindProperty]
@@ -35,7 +39,7 @@ namespace YAPWebApplication.Pages.Views.Components
if (id == null)
return NotFound();
var component = _adapter.GetComponentByData(id);
var component = _componentAdapter.GetComponentByData(id);
if (component == null)
return NotFound();
LoadComponentTypeList();
@@ -45,7 +49,15 @@ namespace YAPWebApplication.Pages.Views.Components
Id = component.Id,
Name = component.Name,
ComponentType = component.ComponentType.ToString(),
ProductSetIds = component.ProductSetIds,
};
var prodSets = _productSetAdapter.GetList();
ProductSetSelectList = prodSets?.Select(c => new SelectListItem
{
Value = c.Id,
Text = $"{c.SetName}",
Selected = Component.ProductSetIds.Contains(c.Id)
}).ToList() ?? new List<SelectListItem>();
return Page();
}
@@ -55,17 +67,19 @@ namespace YAPWebApplication.Pages.Views.Components
if (!ModelState.IsValid)
{
LoadComponentTypeList();
LoadSetsList();
return Page();
}
try
{
_adapter.Update(Component);
_componentAdapter.Update(Component);
return RedirectToPage("./Index");
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Ошибка: {ex.Message}");
LoadComponentTypeList();
LoadSetsList();
return Page();
}
}
@@ -80,6 +94,21 @@ namespace YAPWebApplication.Pages.Views.Components
Text = ct.ToString()
});
}
private void LoadSetsList()
{
var prodSets = _productSetAdapter.GetList();
if (Component.ProductSetIds == null)
{
ProductSetSelectList = new List<SelectListItem>();
return;
}
ProductSetSelectList = prodSets?.Select(c => new SelectListItem
{
Value = c.Id,
Text = $"{c.SetName}",
Selected = Component.ProductSetIds.Contains(c.Id)
}).ToList() ?? new List<SelectListItem>();
}
}
}

View File

@@ -2,10 +2,10 @@
@model YAPWebApplication.Pages.Views.Components.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Components";
}
<h1>Index</h1>
<h1>Components</h1>
@if (User.IsInRole("Storekeeper"))
{
<p>

View File

@@ -2,10 +2,10 @@
@model YAPWebApplication.Pages.Views.ProductOrders.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Product Orders";
}
<h1>Index</h1>
<h1>Product Orders</h1>
@if (User.IsInRole("Storekeeper"))
{
<p>

View File

@@ -50,7 +50,6 @@ namespace YAPWebApplication.Pages.Views.ProductSets
SetName = productset.SetName,
TotalPrice = productset.TotalPrice,
ComponentIds = productset.ComponentIds?.ToList() ?? new List<string>(),
// COMMENTS!!!
};
// список для чекбоксов
var components = _componentAdapter.GetList(true);

View File

@@ -2,10 +2,10 @@
@model YAPWebApplication.Pages.Views.ProductSets.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Product Sets";
}
<h1>Index</h1>
<h1>Product Sets</h1>
@if (User.IsInRole("Worker"))
{
<p>

View File

@@ -2,10 +2,10 @@
@model YAPWebApplication.Pages.Views.Products.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Products";
}
<h1>Index</h1>
<h1>Products</h1>
@if (User.IsInRole("Storekeeper"))
{
<p>

View File

@@ -2,10 +2,10 @@
@model YAPWebApplication.Pages.Views.Purchases.IndexModel
@{
ViewData["Title"] = "Index";
ViewData["Title"] = "Purchases";
}
<h1>Index</h1>
<h1>Purchases</h1>
@if (User.IsInRole("Worker"))
{

View File

@@ -0,0 +1,50 @@
@page
@model YAPWebApplication.Pages.Views.Reports.ComponentsReportModel
@{
ViewData["Title"] = "Report: Components by date";
}
<h1>Components report</h1>
<form method="post">
<div class="form-group">
<label>Start date</label>
<input asp-for="DateStart" type="date" class="form-control" />
</div>
<div class="form-group">
<label>End date</label>
<input asp-for="DateEnd" type="date" class="form-control" />
</div>
<div class="text-danger">
@Html.ValidationSummary(false)
</div>
<button type="submit" class="btn btn-primary">Show report</button>
</form>
@if (Model.ReportUrl != null)
{
<hr />
<h3>PDF preview</h3>
<iframe src="@Model.ReportUrl" width="100%" height="600px"></iframe>
<p class="mt-2">
<a href="@Model.ReportUrl" class="btn btn-success" download>Download PDF</a>
</p>
}
<form method="post" asp-page-handler="SendEmail">
<input type="hidden" asp-for="DateStart" />
<input type="hidden" asp-for="DateEnd" />
<div class="form-group">
<label>Email</label>
<input asp-for="RecipientEmail" type="email" class="form-control" />
</div>
<div class="form-group mt-2">
<button type="submit" class="btn btn-success">Send to Email</button>
</div>
</form>
@if (TempData["Message"] != null)
{
<div class="alert alert-success mt-2">@TempData["Message"]</div>
}

View File

@@ -0,0 +1,150 @@
using DocumentFormat.OpenXml.Bibliography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Security.Claims;
using YAPContracts.AdapterContracts;
using YAPContracts.BusinessLogicContracts;
using YAPWebApplication.Infrastructure;
namespace YAPWebApplication.Pages.Views.Reports
{
internal class ComponentsReportModel : PageModel
{
private readonly IReportBusinessLogicContract _reportBL;
private readonly IEmailService _emailService;
private readonly IStorekeeperAdapter _storekeeperAdapter;
public ComponentsReportModel(
IReportBusinessLogicContract reportBL,
IEmailService emailService,
IStorekeeperAdapter storekeeperAdapter)
{
_reportBL = reportBL;
_emailService = emailService;
_storekeeperAdapter = storekeeperAdapter;
}
[BindProperty(SupportsGet = true)]
public DateTime DateStart { get; set; }
[BindProperty(SupportsGet = true)]
public DateTime DateEnd { get; set; }
[BindProperty]
public string RecipientEmail { get; set; } = string.Empty;
public string? ReportUrl { get; set; }
public IActionResult OnGet(string? reportFile = null)
{
if (DateStart == default)
DateStart = DateTime.UtcNow.ToLocalTime();
if (DateEnd == default)
DateEnd = DateTime.UtcNow.ToLocalTime().AddDays(7);
FillEmail();
if (!string.IsNullOrEmpty(reportFile))
{
ReportUrl = $"/reports/{reportFile}";
}
return Page();
}
public IActionResult OnPost()
{
FillEmail();
if (DateStart > DateEnd)
{
ModelState.AddModelError(string.Empty, "End date must be later than start date!");
return Page();
}
var stream = _reportBL.MakeReportByComponents(DateStart.ToUniversalTime(), DateEnd.ToUniversalTime());
stream.Seek(0, SeekOrigin.Begin);
var fileName = $"components_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf";
var folderPath = Path.Combine("wwwroot", "reports");
Directory.CreateDirectory(folderPath);
foreach (var file in Directory.GetFiles(folderPath, "*.pdf"))
{
var creationTime = System.IO.File.GetCreationTime(file);
if (creationTime < DateTime.Now.AddDays(-1))
{
try { System.IO.File.Delete(file); } catch { }
}
}
var filePath = Path.Combine(folderPath, fileName);
using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
stream.CopyTo(fs);
}
return RedirectToPage(new
{
DateStart = DateStart.ToString("yyyy-MM-dd"),
DateEnd = DateEnd.ToString("yyyy-MM-dd"),
reportFile = fileName
});
}
public async Task<IActionResult> OnPostSendEmail()
{
if (DateStart >= DateEnd)
{
ModelState.AddModelError(string.Empty, "End date must be later than start date!");
FillEmail();
return Page();
}
if (string.IsNullOrWhiteSpace(RecipientEmail))
{
ModelState.AddModelError(string.Empty, "Email is required to send report!");
FillEmail();
return Page();
}
var stream = _reportBL.MakeReportByComponents(DateStart.ToUniversalTime(), DateEnd.ToUniversalTime());
stream.Seek(0, SeekOrigin.Begin);
try
{
await _emailService.SendEmailAsync(
RecipientEmail,
"Components report",
$"Components report for period {DateStart:dd.MM.yyyy} - {DateEnd:dd.MM.yyyy}",
stream,
$"components_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf"
);
TempData["Message"] = "Report sent to Email!";
return RedirectToPage(new
{
DateStart = DateStart.ToString("yyyy-MM-dd"),
DateEnd = DateEnd.ToString("yyyy-MM-dd"),
reportFile = $"components_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf"
});
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Failed to send email: {ex.Message}");
FillEmail();
return Page();
}
}
private void FillEmail()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = _storekeeperAdapter.GetElement(userId);
if (!string.IsNullOrWhiteSpace(user?.Email))
RecipientEmail = user.Email;
}
}
}

View File

@@ -0,0 +1,25 @@
@page
@model YAPWebApplication.Pages.Views.Reports.IndexModel
@{
ViewData["Title"] = "Reports";
}
<h1>Get a report</h1>
@if (User.IsInRole("Worker")) {
<p>
<a asp-page="ProductsByProductSetsReport" class="btn btn-primary">Products by ProductSets</a>
</p>
<p>
<a asp-page="PurchasesReport" class="btn btn-outline-dark">Purchases by date</a>
</p>
}
@if (User.IsInRole("Storekeeper"))
{
<p>
<a asp-page="ProductSetsByProductsReport" class="btn btn-primary">Product Sets by Products</a>
</p>
<p>
<a asp-page="ComponentsReport" class="btn btn-outline-dark">Components by date</a>
</p>
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace YAPWebApplication.Pages.Views.Reports
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,37 @@
@page
@model YAPWebApplication.Pages.Views.Reports.ProductSetsByProductsReportModel
@{
ViewData["Title"] = "Report: Sets by Products";
}
<h1>Report: sets by products</h1>
<form method="post">
<div class="form-group">
<label>Choose a product</label>
@foreach (var prod in Model.Products)
{
<div class="form-check">
<input type="checkbox"
name="SelectedProductIds"
value="@prod.Value"
class="form-check-input"
id="prod_@prod.Value" />
<label class="form-check-label" for="prod_@prod.Value">@prod.Text</label>
</div>
}
</div>
<div class="form-group mt-3">
<div class="form-group">
<label>Format</label>
<select asp-for="Format" class="form-control">
<option value="doc">Word (.docx)</option>
<option value="xls">Excel (.xlsx)</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Download</button>
<a asp-page="./Index" class="btn btn-secondary">Back to List</a>
</div>
</form>

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using YAPContracts.AdapterContracts;
using YAPContracts.BusinessLogicContracts;
namespace YAPWebApplication.Pages.Views.Reports
{
internal class ProductSetsByProductsReportModel : PageModel
{
private readonly IReportBusinessLogicContract _reportBL;
private readonly IProductAdapter _productAdapter;
public ProductSetsByProductsReportModel(IReportBusinessLogicContract reportBL, IProductAdapter productAdapter)
{
_reportBL = reportBL;
_productAdapter = productAdapter;
}
public List<SelectListItem> Products { get; set; } = new();
[BindProperty]
public List<string> SelectedProductIds { get; set; } = new();
[BindProperty]
public string Format { get; set; } = "doc";
public IActionResult OnGet()
{
LoadFormFields();
return Page();
}
public IActionResult OnPost()
{
var stream = _reportBL.MakeReportProductSetsByProducts(SelectedProductIds, Format);
stream.Seek(0, SeekOrigin.Begin);
var fileName = $"sets_by_products.{Format}";
var contentType = Format == "xls"
? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
return File(stream, contentType, fileName);
}
private void LoadFormFields()
{
Products = _productAdapter.GetList()?
.Select(p => new SelectListItem { Value = p.Id, Text = p.Name })
.ToList() ?? new();
}
}
}

View File

@@ -0,0 +1,50 @@
@page
@model YAPWebApplication.Pages.Views.Reports.ProductsByProductSetsReportModel
@{
ViewData["Title"] = "Report: Products by Sets";
}
<h1>Products By Product Sets Report</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label>Product Sets</label>
@foreach (var set in Model.ProductSets)
{
<div class="form-check">
<input type="checkbox"
name="SelectedProductSetIds"
value="@set.Value"
class="form-check-input"
id="set_@set.Value" />
<label class="form-check-label" for="set_@set.Value">@set.Text</label>
</div>
}
</div>
<div class="form-group mt-3">
<div class="form-group">
<label>Format</label>
<select asp-for="Format" class="form-control">
<option value="doc">Word (.docx)</option>
<option value="xls">Excel (.xlsx)</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Download</button>
<a asp-page="./Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}

View File

@@ -0,0 +1,62 @@
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics.Contracts;
using System.Security.Claims;
using YAPContracts.AdapterContracts;
using YAPContracts.BindingModels;
using YAPContracts.BusinessLogicContracts;
using YAPContracts.Enums;
using YAPDatabase.Models;
using YAPWebApplication.Adapters;
namespace YAPWebApplication.Pages.Views.Reports
{
internal class ProductsByProductSetsReportModel : PageModel
{
private readonly IProductSetAdapter _productSetAdapter;
private readonly IReportBusinessLogicContract _report;
public ProductsByProductSetsReportModel(IProductSetAdapter productSetAdapter, IReportBusinessLogicContract report)
{
_productSetAdapter = productSetAdapter;
_report = report;
}
[BindProperty]
public List<string> SelectedProductSetIds { get; set; } = new();
public List<SelectListItem> ProductSets { get; set; } = new();
[BindProperty]
public string Format { get; set; } = "doc";
public IActionResult OnGet()
{
LoadFormFields();
return Page();
}
public IActionResult OnPost()
{
var stream = _report.MakeReportProductsByProductSets(SelectedProductSetIds, Format);
stream.Seek(0, SeekOrigin.Begin);
var fileName = $"products_by_sets.{Format}";
var contentType = Format == "xls"
? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
return File(stream, contentType, fileName);
}
private void LoadFormFields()
{
ProductSets = _productSetAdapter.GetList()?
.Select(s => new SelectListItem { Value = s.Id, Text = s.SetName })
.ToList() ?? new();
}
}
}

View File

@@ -0,0 +1,48 @@
@page
@model YAPWebApplication.Pages.Views.Reports.PurchasesReportModel
@{
ViewData["Title"] = "Report: Purchases by date";
}
<h1>Purchases report</h1>
<form method="post">
<div class="form-group">
<label>Start date</label>
<input asp-for="DateStart" type="date" class="form-control" />
</div>
<div class="form-group">
<label>End date</label>
<input asp-for="DateEnd" type="date" class="form-control" />
</div>
<div class="text-danger">
@Html.ValidationSummary(false)
</div>
<button type="submit" class="btn btn-primary">Show report</button>
</form>
@if (Model.ReportUrl != null)
{
<hr />
<h3>PDF preview</h3>
<iframe src="@Model.ReportUrl" width="100%" height="600px"></iframe>
<p class="mt-2">
<a href="@Model.ReportUrl" class="btn btn-success" download>Download PDF</a>
</p>
}
<form method="post" asp-page-handler="SendEmail">
<input type="hidden" asp-for="DateStart" />
<input type="hidden" asp-for="DateEnd" />
<div class="form-group">
<label>Email</label>
<input asp-for="RecipientEmail" type="email" class="form-control" />
</div>
<div class="form-group mt-2">
<button type="submit" class="btn btn-success">Send to Email</button>
</div>
</form>
@if (TempData["Message"] != null)
{
<div class="alert alert-success mt-2">@TempData["Message"]</div>
}

View File

@@ -0,0 +1,145 @@
using DocumentFormat.OpenXml.Bibliography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Security.Claims;
using YAPContracts.AdapterContracts;
using YAPContracts.BusinessLogicContracts;
using YAPWebApplication.Infrastructure;
namespace YAPWebApplication.Pages.Views.Reports
{
internal class PurchasesReportModel : PageModel
{
private readonly IReportBusinessLogicContract _reportBL;
private readonly IEmailService _emailService;
private readonly IWorkerAdapter _workerAdapter;
public PurchasesReportModel(IReportBusinessLogicContract reportBL, IEmailService emailService, IWorkerAdapter workerAdapter)
{
_reportBL = reportBL;
_emailService = emailService;
_workerAdapter = workerAdapter;
}
// supporting query string on GET
[BindProperty(SupportsGet = true)]
public DateTime DateStart { get; set; }
[BindProperty(SupportsGet = true)]
public DateTime DateEnd { get; set; }
[BindProperty]
public string RecipientEmail { get; set; } = string.Empty;
public string? ReportUrl { get; set; }
public IActionResult OnGet(string? reportFile = null)
{
if (DateStart == default) DateStart = DateTime.UtcNow.ToLocalTime();
if (DateEnd == default) DateEnd = DateTime.UtcNow.ToLocalTime().AddDays(7);
FillEmail();
if (!string.IsNullOrEmpty(reportFile))
{
ReportUrl = $"/reports/{reportFile}";
}
return Page();
}
public IActionResult OnPost()
{
FillEmail();
if (DateStart > DateEnd)
{
ModelState.AddModelError(string.Empty, "End date must be later than start date!.");
return Page();
}
var stream = _reportBL.MakeReportByPurchases(DateStart.ToUniversalTime(), DateEnd.ToUniversalTime());
stream.Seek(0, SeekOrigin.Begin);
var fileName = $"sales_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf";
var folderPath = Path.Combine("wwwroot", "reports");
Directory.CreateDirectory(folderPath);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
foreach (var file in Directory.GetFiles(folderPath, "*.pdf"))
{
var creationTime = System.IO.File.GetCreationTime(file);
if (creationTime < DateTime.Now.AddDays(-1))
{
try { System.IO.File.Delete(file); } catch { /* ignore */ }
}
}
var filePath = Path.Combine(folderPath, fileName);
using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
stream.CopyTo(fs);
}
// PRG: redirecting on GET, to save dates and filename in query
return RedirectToPage(new
{
DateStart = DateStart.ToString("yyyy-MM-dd"),
DateEnd = DateEnd.ToString("yyyy-MM-dd"),
reportFile = fileName
});
}
public async Task<IActionResult> OnPostSendEmail()
{
if (DateStart >= DateEnd)
{
ModelState.AddModelError(string.Empty, "End date must be later than start date!");
FillEmail();
return Page();
}
if (string.IsNullOrWhiteSpace(RecipientEmail))
{
ModelState.AddModelError(string.Empty, "Email is required to send report!");
FillEmail();
return Page();
}
var stream = _reportBL.MakeReportByPurchases(DateStart.ToUniversalTime(), DateEnd.ToUniversalTime());
stream.Seek(0, SeekOrigin.Begin);
try
{
await _emailService.SendEmailAsync(
RecipientEmail,
"Purchases report",
$"Report for period {DateStart:dd.MM.yyyy} - {DateEnd:dd.MM.yyyy}",
stream,
$"sales_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf"
);
TempData["Message"] = "Report sent to Email!";
return RedirectToPage(new
{
DateStart = DateStart.ToString("yyyy-MM-dd"),
DateEnd = DateEnd.ToString("yyyy-MM-dd"),
reportFile = $"sales_report_{DateStart:yyyyMMdd}_{DateEnd:yyyyMMdd}.pdf"
});
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Failed to send email: {ex.Message}");
FillEmail();
return Page();
}
}
private void FillEmail()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = _workerAdapter.GetElement(userId);
if (!string.IsNullOrWhiteSpace(user?.Email))
RecipientEmail = user.Email;
}
}
}

View File

@@ -5,6 +5,7 @@ using Serilog;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using YAPBusinessLogic.Implementations;
using YAPBusinessLogic.OfficePackage;
using YAPContracts.AdapterContracts;
using YAPContracts.BusinessLogicContracts;
using YAPContracts.Infrastructure;
@@ -66,6 +67,10 @@ builder.Services.AddTransient<IProductSetBusinessLogicContract, ProductSetBusine
builder.Services.AddTransient<IWorkerBusinessLogicContract, WorkerBusinessLogicContract>();
builder.Services.AddTransient<IStorekeeperBusinessLogicContract, StorekeeperBusinessLogicContract>();
builder.Services.AddTransient<BasePdfBuilder, MigraDocPdfBuilder>();
builder.Services.AddTransient<BaseWordBuilder, OpenXmlWordBuilder>();
builder.Services.AddTransient<BaseExcelBuilder, OpenXmlExcelBuilder>();
builder.Services.AddTransient<ICommentStorageContract, CommentStorageContract>();
builder.Services.AddTransient<IComponentStorageContract, ComponentStorageContract>();
builder.Services.AddTransient<IPurchaseStorageContract, PurchaseStorageContract>();
@@ -84,6 +89,8 @@ builder.Services.AddTransient<IPurchaseAdapter, PurchaseAdapter>();
builder.Services.AddTransient<IWorkerAdapter, WorkerAdapter>();
builder.Services.AddTransient<IStorekeeperAdapter, StorekeeperAdapter>();
builder.Services.AddTransient<IReportBusinessLogicContract, ReportBusinessLogicContract>();
builder.Services.AddTransient<IEmailService, EmailService>();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />

View File

@@ -24,5 +24,13 @@
"AllowedHosts": "*",
"DatabaseSettings": {
"ConnectionString": "Host=127.0.0.1;Port=5432;Database=YAP;Username=postgres;Password=postgres;"
},
"Email": {
"SmtpServer": "smtp.yandex.ru",
"SmtpPort": 465,
"SmtpUser": "saratov.escaper@yandex.ru",
"SmtpPass": "gldtzvjtnfgzhhpu",
"FromName": "YAP Mail Service",
"FromEmail": "saratov.escaper@yandex.ru"
}
}