From 7f990d6f7d845916e226c022a80b82937986c221 Mon Sep 17 00:00:00 2001 From: Erman Date: Wed, 9 Sep 2026 03:57:03 +0500 Subject: [PATCH] DEMO shop: simplified project with comments --- .gitignore | 8 + DEMO.slnx | 3 + DEMO/Controllers/AuthController.cs | 76 ++ DEMO/Controllers/CategoriesController.cs | 67 ++ DEMO/Controllers/OrderItemsController.cs | 77 ++ DEMO/Controllers/OrdersController.cs | 75 ++ DEMO/Controllers/ProductsController.cs | 85 ++ DEMO/Controllers/RolesController.cs | 67 ++ DEMO/Controllers/StocksController.cs | 76 ++ DEMO/Controllers/UsersController.cs | 75 ++ DEMO/Controllers/WarehousesController.cs | 70 ++ DEMO/DEMO.csproj | 18 + DEMO/DEMO.http | 6 + DEMO/Data/AppDbContext.cs | 23 + DEMO/Data/AppDbContextFactory.cs | 20 + .../20260908211451_InitialCreate.Designer.cs | 46 + .../20260908211451_InitialCreate.cs | 34 + .../20260908211931_AddProducts.Designer.cs | 82 ++ DEMO/Migrations/20260908211931_AddProducts.cs | 47 + .../20260908215804_AddWarehouses.Designer.cs | 103 ++ .../20260908215804_AddWarehouses.cs | 35 + .../20260908220130_AddStock.Designer.cs | 148 +++ DEMO/Migrations/20260908220130_AddStock.cs | 58 ++ ...0260908220723_AddUsersAndRoles.Designer.cs | 207 +++++ .../20260908220723_AddUsersAndRoles.cs | 63 ++ .../20260908221344_AddOrders.Designer.cs | 239 +++++ DEMO/Migrations/20260908221344_AddOrders.cs | 48 + .../20260908221738_AddOrderItems.Designer.cs | 287 ++++++ .../20260908221738_AddOrderItems.cs | 59 ++ DEMO/Migrations/AppDbContextModelSnapshot.cs | 284 ++++++ DEMO/Models/Category.cs | 9 + DEMO/Models/Order.cs | 15 + DEMO/Models/OrderItem.cs | 19 + DEMO/Models/Product.cs | 20 + DEMO/Models/Role.cs | 9 + DEMO/Models/Stock.cs | 17 + DEMO/Models/User.cs | 15 + DEMO/Models/Warehouse.cs | 11 + DEMO/Program.cs | 34 + DEMO/Properties/launchSettings.json | 23 + DEMO/appsettings.Development.json | 8 + DEMO/appsettings.json | 12 + DEMO/wwwroot/index.html | 160 ++++ DEMO/wwwroot/script.js | 877 ++++++++++++++++++ DEMO/wwwroot/style.css | 496 ++++++++++ 45 files changed, 4211 insertions(+) create mode 100644 .gitignore create mode 100644 DEMO.slnx create mode 100644 DEMO/Controllers/AuthController.cs create mode 100644 DEMO/Controllers/CategoriesController.cs create mode 100644 DEMO/Controllers/OrderItemsController.cs create mode 100644 DEMO/Controllers/OrdersController.cs create mode 100644 DEMO/Controllers/ProductsController.cs create mode 100644 DEMO/Controllers/RolesController.cs create mode 100644 DEMO/Controllers/StocksController.cs create mode 100644 DEMO/Controllers/UsersController.cs create mode 100644 DEMO/Controllers/WarehousesController.cs create mode 100644 DEMO/DEMO.csproj create mode 100644 DEMO/DEMO.http create mode 100644 DEMO/Data/AppDbContext.cs create mode 100644 DEMO/Data/AppDbContextFactory.cs create mode 100644 DEMO/Migrations/20260908211451_InitialCreate.Designer.cs create mode 100644 DEMO/Migrations/20260908211451_InitialCreate.cs create mode 100644 DEMO/Migrations/20260908211931_AddProducts.Designer.cs create mode 100644 DEMO/Migrations/20260908211931_AddProducts.cs create mode 100644 DEMO/Migrations/20260908215804_AddWarehouses.Designer.cs create mode 100644 DEMO/Migrations/20260908215804_AddWarehouses.cs create mode 100644 DEMO/Migrations/20260908220130_AddStock.Designer.cs create mode 100644 DEMO/Migrations/20260908220130_AddStock.cs create mode 100644 DEMO/Migrations/20260908220723_AddUsersAndRoles.Designer.cs create mode 100644 DEMO/Migrations/20260908220723_AddUsersAndRoles.cs create mode 100644 DEMO/Migrations/20260908221344_AddOrders.Designer.cs create mode 100644 DEMO/Migrations/20260908221344_AddOrders.cs create mode 100644 DEMO/Migrations/20260908221738_AddOrderItems.Designer.cs create mode 100644 DEMO/Migrations/20260908221738_AddOrderItems.cs create mode 100644 DEMO/Migrations/AppDbContextModelSnapshot.cs create mode 100644 DEMO/Models/Category.cs create mode 100644 DEMO/Models/Order.cs create mode 100644 DEMO/Models/OrderItem.cs create mode 100644 DEMO/Models/Product.cs create mode 100644 DEMO/Models/Role.cs create mode 100644 DEMO/Models/Stock.cs create mode 100644 DEMO/Models/User.cs create mode 100644 DEMO/Models/Warehouse.cs create mode 100644 DEMO/Program.cs create mode 100644 DEMO/Properties/launchSettings.json create mode 100644 DEMO/appsettings.Development.json create mode 100644 DEMO/appsettings.json create mode 100644 DEMO/wwwroot/index.html create mode 100644 DEMO/wwwroot/script.js create mode 100644 DEMO/wwwroot/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e707d33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# билд +bin/ +obj/ +# студия +.vs/ +# юзерские настройки +*.user +*.suo diff --git a/DEMO.slnx b/DEMO.slnx new file mode 100644 index 0000000..b559f4c --- /dev/null +++ b/DEMO.slnx @@ -0,0 +1,3 @@ + + + diff --git a/DEMO/Controllers/AuthController.cs b/DEMO/Controllers/AuthController.cs new file mode 100644 index 0000000..1b91a71 --- /dev/null +++ b/DEMO/Controllers/AuthController.cs @@ -0,0 +1,76 @@ +// вход и регистрация +using DEMO.Data; // база +using DEMO.Models; // пользователи +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // поиск в базе + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/auth +public class AuthController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public AuthController(AppDbContext context) + { + _context = context; + } + + // войти, адрес POST api/auth/login + [HttpPost("login")] + public async Task Login(User user) // приходят логин и пароль + { + // ищу такого человека в базе + var existingUser = await _context.Users + .Include(u => u.Role) // сразу беру его роль + .FirstOrDefaultAsync(u => + u.Login == user.Login && + u.Password == user.Password); + + // не нашел значит неправильный пароль + if (existingUser == null) + return Unauthorized("Неверный логин или пароль."); + + // нашел, отдаю его обратно + return Ok(new + { + existingUser.Id, + existingUser.Login, + Role = existingUser.Role?.Name + }); + } + + // регистрация, адрес POST api/auth/register + [HttpPost("register")] + public async Task Register(User user) // приходят логин и пароль + { + // проверяю нет ли уже такого логина + var exists = await _context.Users + .AnyAsync(u => u.Login == user.Login); + + if (exists) + return BadRequest("Такой логин уже занят."); + + // ищу роль клиент чтоб дать ее новичку + var clientRole = await _context.Roles + .FirstOrDefaultAsync(r => r.Name == "Client"); + + if (clientRole == null) + return BadRequest("Роль Client не найдена."); + + user.RoleId = clientRole.Id; // даю роль клиент + + _context.Users.Add(user); // добавляю в базу + await _context.SaveChangesAsync(); // сохраняю + + // отдаю нового человека + return Ok(new + { + user.Id, + user.Login, + Role = clientRole.Name + }); + } +} diff --git a/DEMO/Controllers/CategoriesController.cs b/DEMO/Controllers/CategoriesController.cs new file mode 100644 index 0000000..051da9c --- /dev/null +++ b/DEMO/Controllers/CategoriesController.cs @@ -0,0 +1,67 @@ +// все про категории +using DEMO.Data; // база +using DEMO.Models; // категории +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/categories +public class CategoriesController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public CategoriesController(AppDbContext context) + { + _context = context; + } + + // отдать все категории + [HttpGet] + public async Task GetCategories() + { + var categories = await _context.Categories.ToListAsync(); // беру все + return Ok(categories); // отдаю + } + + // добавить категорию + [HttpPost] + public async Task CreateCategory(Category category) // приходит название + { + _context.Categories.Add(category); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(category); + } + + // изменить категорию + [HttpPut("{id}")] + public async Task UpdateCategory(int id, Category category) + { + var existingCategory = await _context.Categories.FindAsync(id); // ищу старую + + if (existingCategory == null) + return NotFound(); // нет такой + + existingCategory.Name = category.Name; // меняю название + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingCategory); + } + + // удалить категорию + [HttpDelete("{id}")] + public async Task DeleteCategory(int id) + { + var category = await _context.Categories.FindAsync(id); // ищу + + if (category == null) + return NotFound(); + + _context.Categories.Remove(category); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/OrderItemsController.cs b/DEMO/Controllers/OrderItemsController.cs new file mode 100644 index 0000000..610713e --- /dev/null +++ b/DEMO/Controllers/OrderItemsController.cs @@ -0,0 +1,77 @@ +// товары внутри заказов +using DEMO.Data; // база +using DEMO.Models; // позиции заказа +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/orderitems +public class OrderItemsController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public OrderItemsController(AppDbContext context) + { + _context = context; + } + + // отдать все позиции + [HttpGet] + public async Task GetOrderItems() + { + // беру позиции и сразу заказ и товар + var orderItems = await _context.OrderItems + .Include(i => i.Order) + .Include(i => i.Product) + .ToListAsync(); + + return Ok(orderItems); + } + + // добавить товар в заказ + [HttpPost] + public async Task CreateOrderItem(OrderItem orderItem) // приходит заказ товар колво цена + { + _context.OrderItems.Add(orderItem); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(orderItem); + } + + // изменить позицию + [HttpPut("{id}")] + public async Task UpdateOrderItem(int id, OrderItem orderItem) + { + var existingItem = await _context.OrderItems.FindAsync(id); // ищу старую + + if (existingItem == null) + return NotFound(); + + // меняю поля + existingItem.OrderId = orderItem.OrderId; + existingItem.ProductId = orderItem.ProductId; + existingItem.Quantity = orderItem.Quantity; + existingItem.Price = orderItem.Price; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingItem); + } + + // удалить позицию + [HttpDelete("{id}")] + public async Task DeleteOrderItem(int id) + { + var orderItem = await _context.OrderItems.FindAsync(id); // ищу + + if (orderItem == null) + return NotFound(); + + _context.OrderItems.Remove(orderItem); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/OrdersController.cs b/DEMO/Controllers/OrdersController.cs new file mode 100644 index 0000000..ea8a57e --- /dev/null +++ b/DEMO/Controllers/OrdersController.cs @@ -0,0 +1,75 @@ +// все про заказы +using DEMO.Data; // база +using DEMO.Models; // заказы +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/orders +public class OrdersController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public OrdersController(AppDbContext context) + { + _context = context; + } + + // отдать все заказы + [HttpGet] + public async Task GetOrders() + { + // беру заказы и кто их сделал + var orders = await _context.Orders + .Include(o => o.User) + .ToListAsync(); + + return Ok(orders); + } + + // добавить заказ + [HttpPost] + public async Task CreateOrder(Order order) // приходит человек дата статус + { + _context.Orders.Add(order); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(order); + } + + // изменить заказ + [HttpPut("{id}")] + public async Task UpdateOrder(int id, Order order) + { + var existingOrder = await _context.Orders.FindAsync(id); // ищу старый + + if (existingOrder == null) + return NotFound(); + + // меняю поля + existingOrder.UserId = order.UserId; + existingOrder.Date = order.Date; + existingOrder.Status = order.Status; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingOrder); + } + + // удалить заказ + [HttpDelete("{id}")] + public async Task DeleteOrder(int id) + { + var order = await _context.Orders.FindAsync(id); // ищу + + if (order == null) + return NotFound(); + + _context.Orders.Remove(order); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/ProductsController.cs b/DEMO/Controllers/ProductsController.cs new file mode 100644 index 0000000..18caa5e --- /dev/null +++ b/DEMO/Controllers/ProductsController.cs @@ -0,0 +1,85 @@ +// все про товары +using DEMO.Data; // база +using DEMO.Models; // товары +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // поиск в базе + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/products +public class ProductsController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public ProductsController(AppDbContext context) + { + _context = context; + } + + // отдать все товары, GET api/products + [HttpGet] + public async Task GetProducts() + { + // беру товары и сразу их категории + var products = await _context.Products + .Include(p => p.Category) + .ToListAsync(); + + return Ok(products); // отдаю список + } + + // добавить товар, POST api/products + [HttpPost] + public async Task CreateProduct(Product product) // приходит товар + { + // проверяю есть ли такая категория + var categoryExists = await _context.Categories + .AnyAsync(c => c.Id == product.CategoryId); + + if (!categoryExists) + return BadRequest("Такой категории нет."); + + _context.Products.Add(product); // добавляю + await _context.SaveChangesAsync(); // сохраняю + + return Ok(product); // отдаю обратно + } + + // изменить товар, PUT api/products/5 + [HttpPut("{id}")] + public async Task UpdateProduct(int id, Product product) + { + // ищу старый товар + var existingProduct = await _context.Products.FindAsync(id); + + if (existingProduct == null) + return NotFound(); // нет такого + + // меняю поля + existingProduct.Name = product.Name; + existingProduct.Price = product.Price; + existingProduct.CategoryId = product.CategoryId; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingProduct); + } + + // удалить товар, DELETE api/products/5 + [HttpDelete("{id}")] + public async Task DeleteProduct(int id) + { + // ищу товар + var product = await _context.Products.FindAsync(id); + + if (product == null) + return NotFound(); + + _context.Products.Remove(product); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); // все хорошо, отдавать нечего + } +} diff --git a/DEMO/Controllers/RolesController.cs b/DEMO/Controllers/RolesController.cs new file mode 100644 index 0000000..b2c617c --- /dev/null +++ b/DEMO/Controllers/RolesController.cs @@ -0,0 +1,67 @@ +// все про роли (админ клиент и тд) +using DEMO.Data; // база +using DEMO.Models; // роли +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/roles +public class RolesController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public RolesController(AppDbContext context) + { + _context = context; + } + + // отдать все роли + [HttpGet] + public async Task GetRoles() + { + var roles = await _context.Roles.ToListAsync(); // беру все + return Ok(roles); + } + + // добавить роль + [HttpPost] + public async Task CreateRole(Role role) // приходит название + { + _context.Roles.Add(role); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(role); + } + + // изменить роль + [HttpPut("{id}")] + public async Task UpdateRole(int id, Role role) + { + var existingRole = await _context.Roles.FindAsync(id); // ищу старую + + if (existingRole == null) + return NotFound(); + + existingRole.Name = role.Name; // меняю название + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingRole); + } + + // удалить роль + [HttpDelete("{id}")] + public async Task DeleteRole(int id) + { + var role = await _context.Roles.FindAsync(id); // ищу + + if (role == null) + return NotFound(); + + _context.Roles.Remove(role); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/StocksController.cs b/DEMO/Controllers/StocksController.cs new file mode 100644 index 0000000..0c3998b --- /dev/null +++ b/DEMO/Controllers/StocksController.cs @@ -0,0 +1,76 @@ +// все про остатки на складах +using DEMO.Data; // база +using DEMO.Models; // остатки +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/stocks +public class StocksController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public StocksController(AppDbContext context) + { + _context = context; + } + + // отдать все остатки + [HttpGet] + public async Task GetStocks() + { + // беру остатки и сразу товары и склады чтоб показать названия + var stocks = await _context.Stocks + .Include(s => s.Product) + .Include(s => s.Warehouse) + .ToListAsync(); + + return Ok(stocks); + } + + // добавить остаток + [HttpPost] + public async Task CreateStock(Stock stock) // приходит товар склад и количество + { + _context.Stocks.Add(stock); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(stock); + } + + // изменить остаток + [HttpPut("{id}")] + public async Task UpdateStock(int id, Stock stock) + { + var existingStock = await _context.Stocks.FindAsync(id); // ищу старый + + if (existingStock == null) + return NotFound(); + + // меняю поля + existingStock.ProductId = stock.ProductId; + existingStock.WarehouseId = stock.WarehouseId; + existingStock.Quantity = stock.Quantity; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingStock); + } + + // удалить остаток + [HttpDelete("{id}")] + public async Task DeleteStock(int id) + { + var stock = await _context.Stocks.FindAsync(id); // ищу + + if (stock == null) + return NotFound(); + + _context.Stocks.Remove(stock); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/UsersController.cs b/DEMO/Controllers/UsersController.cs new file mode 100644 index 0000000..b8939e5 --- /dev/null +++ b/DEMO/Controllers/UsersController.cs @@ -0,0 +1,75 @@ +// все про пользователей +using DEMO.Data; // база +using DEMO.Models; // пользователи +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/users +public class UsersController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public UsersController(AppDbContext context) + { + _context = context; + } + + // отдать всех людей + [HttpGet] + public async Task GetUsers() + { + // беру людей и их роли + var users = await _context.Users + .Include(u => u.Role) + .ToListAsync(); + + return Ok(users); + } + + // добавить человека + [HttpPost] + public async Task CreateUser(User user) // приходит логин пароль роль + { + _context.Users.Add(user); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(user); + } + + // изменить человека + [HttpPut("{id}")] + public async Task UpdateUser(int id, User user) + { + var existingUser = await _context.Users.FindAsync(id); // ищу старого + + if (existingUser == null) + return NotFound(); + + // меняю поля + existingUser.Login = user.Login; + existingUser.Password = user.Password; + existingUser.RoleId = user.RoleId; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingUser); + } + + // удалить человека + [HttpDelete("{id}")] + public async Task DeleteUser(int id) + { + var user = await _context.Users.FindAsync(id); // ищу + + if (user == null) + return NotFound(); + + _context.Users.Remove(user); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/Controllers/WarehousesController.cs b/DEMO/Controllers/WarehousesController.cs new file mode 100644 index 0000000..2c727ca --- /dev/null +++ b/DEMO/Controllers/WarehousesController.cs @@ -0,0 +1,70 @@ +// все про склады +using DEMO.Data; // база +using DEMO.Models; // склады +using Microsoft.AspNetCore.Mvc; // контроллер +using Microsoft.EntityFrameworkCore; // база + +namespace DEMO.Controllers; + +[ApiController] // это api +[Route("api/[controller]")] // адрес будет api/warehouses +public class WarehousesController : ControllerBase +{ + private readonly AppDbContext _context; // база + + // запоминаю базу + public WarehousesController(AppDbContext context) + { + _context = context; + } + + // отдать все склады + [HttpGet] + public async Task GetWarehouses() + { + var warehouses = await _context.Warehouses.ToListAsync(); // беру все + return Ok(warehouses); + } + + // добавить склад + [HttpPost] + public async Task CreateWarehouse(Warehouse warehouse) // приходит название и адрес + { + _context.Warehouses.Add(warehouse); // добавляю + await _context.SaveChangesAsync(); // сохраняю + return Ok(warehouse); + } + + // изменить склад + [HttpPut("{id}")] + public async Task UpdateWarehouse(int id, Warehouse warehouse) + { + var existingWarehouse = await _context.Warehouses.FindAsync(id); // ищу старый + + if (existingWarehouse == null) + return NotFound(); + + // меняю поля + existingWarehouse.Name = warehouse.Name; + existingWarehouse.Address = warehouse.Address; + + await _context.SaveChangesAsync(); // сохраняю + + return Ok(existingWarehouse); + } + + // удалить склад + [HttpDelete("{id}")] + public async Task DeleteWarehouse(int id) + { + var warehouse = await _context.Warehouses.FindAsync(id); // ищу + + if (warehouse == null) + return NotFound(); + + _context.Warehouses.Remove(warehouse); // удаляю + await _context.SaveChangesAsync(); // сохраняю + + return NoContent(); + } +} diff --git a/DEMO/DEMO.csproj b/DEMO/DEMO.csproj new file mode 100644 index 0000000..021abc7 --- /dev/null +++ b/DEMO/DEMO.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/DEMO/DEMO.http b/DEMO/DEMO.http new file mode 100644 index 0000000..5ed259c --- /dev/null +++ b/DEMO/DEMO.http @@ -0,0 +1,6 @@ +@DEMO_HostAddress = http://localhost:5214 + +GET {{DEMO_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/DEMO/Data/AppDbContext.cs b/DEMO/Data/AppDbContext.cs new file mode 100644 index 0000000..f70d81f --- /dev/null +++ b/DEMO/Data/AppDbContext.cs @@ -0,0 +1,23 @@ +// это моя база данных, тут все таблицы +using DEMO.Models; // мои классы +using Microsoft.EntityFrameworkCore; // сама база + +namespace DEMO.Data; + +public class AppDbContext : DbContext // главный класс базы +{ + // конструктор, просто передает настройки дальше + public AppDbContext(DbContextOptions options) : base(options) + { + } + + // каждая строчка это одна таблица в базе + public DbSet Categories { get; set; } // категории + public DbSet Products { get; set; } // товары + public DbSet Warehouses { get; set; } // склады + public DbSet Stocks { get; set; } // остатки + public DbSet Roles { get; set; } // роли + public DbSet Users { get; set; } // пользователи + public DbSet Orders { get; set; } // заказы + public DbSet OrderItems { get; set; } // товары в заказах +} diff --git a/DEMO/Data/AppDbContextFactory.cs b/DEMO/Data/AppDbContextFactory.cs new file mode 100644 index 0000000..c1497cd --- /dev/null +++ b/DEMO/Data/AppDbContextFactory.cs @@ -0,0 +1,20 @@ +// это нужно чтоб миграции знали как подключиться к базе +using Microsoft.EntityFrameworkCore; // работа с базой +using Microsoft.EntityFrameworkCore.Design; // для миграций + +namespace DEMO.Data; + +public class AppDbContextFactory : IDesignTimeDbContextFactory // фабрика для создания базы +{ + // создаю базу с моей строкой подключения + public AppDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); // настройки + + // говорю где лежит база (у меня на компе) + optionsBuilder.UseSqlServer( + "Server=(localdb)\\MSSQLLocalDB;Database=DemoDB;Trusted_Connection=True;TrustServerCertificate=True;"); + + return new AppDbContext(optionsBuilder.Options); // отдаю готовую базу + } +} diff --git a/DEMO/Migrations/20260908211451_InitialCreate.Designer.cs b/DEMO/Migrations/20260908211451_InitialCreate.Designer.cs new file mode 100644 index 0000000..2de8e6e --- /dev/null +++ b/DEMO/Migrations/20260908211451_InitialCreate.Designer.cs @@ -0,0 +1,46 @@ +// +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908211451_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908211451_InitialCreate.cs b/DEMO/Migrations/20260908211451_InitialCreate.cs new file mode 100644 index 0000000..108adad --- /dev/null +++ b/DEMO/Migrations/20260908211451_InitialCreate.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/DEMO/Migrations/20260908211931_AddProducts.Designer.cs b/DEMO/Migrations/20260908211931_AddProducts.Designer.cs new file mode 100644 index 0000000..d093e1e --- /dev/null +++ b/DEMO/Migrations/20260908211931_AddProducts.Designer.cs @@ -0,0 +1,82 @@ +// +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908211931_AddProducts")] + partial class AddProducts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908211931_AddProducts.cs b/DEMO/Migrations/20260908211931_AddProducts.cs new file mode 100644 index 0000000..3938e69 --- /dev/null +++ b/DEMO/Migrations/20260908211931_AddProducts.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddProducts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + CategoryId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Products"); + } + } +} diff --git a/DEMO/Migrations/20260908215804_AddWarehouses.Designer.cs b/DEMO/Migrations/20260908215804_AddWarehouses.Designer.cs new file mode 100644 index 0000000..217cc78 --- /dev/null +++ b/DEMO/Migrations/20260908215804_AddWarehouses.Designer.cs @@ -0,0 +1,103 @@ +// +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908215804_AddWarehouses")] + partial class AddWarehouses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908215804_AddWarehouses.cs b/DEMO/Migrations/20260908215804_AddWarehouses.cs new file mode 100644 index 0000000..5e0ba32 --- /dev/null +++ b/DEMO/Migrations/20260908215804_AddWarehouses.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddWarehouses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Warehouses", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Address = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Warehouses", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Warehouses"); + } + } +} diff --git a/DEMO/Migrations/20260908220130_AddStock.Designer.cs b/DEMO/Migrations/20260908220130_AddStock.Designer.cs new file mode 100644 index 0000000..da4e3dd --- /dev/null +++ b/DEMO/Migrations/20260908220130_AddStock.Designer.cs @@ -0,0 +1,148 @@ +// +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908220130_AddStock")] + partial class AddStock + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908220130_AddStock.cs b/DEMO/Migrations/20260908220130_AddStock.cs new file mode 100644 index 0000000..f266ed4 --- /dev/null +++ b/DEMO/Migrations/20260908220130_AddStock.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddStock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Stocks", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProductId = table.Column(type: "int", nullable: false), + WarehouseId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Stocks", x => x.Id); + table.ForeignKey( + name: "FK_Stocks_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Stocks_Warehouses_WarehouseId", + column: x => x.WarehouseId, + principalTable: "Warehouses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Stocks_ProductId", + table: "Stocks", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Stocks_WarehouseId", + table: "Stocks", + column: "WarehouseId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Stocks"); + } + } +} diff --git a/DEMO/Migrations/20260908220723_AddUsersAndRoles.Designer.cs b/DEMO/Migrations/20260908220723_AddUsersAndRoles.Designer.cs new file mode 100644 index 0000000..31e077f --- /dev/null +++ b/DEMO/Migrations/20260908220723_AddUsersAndRoles.Designer.cs @@ -0,0 +1,207 @@ +// +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908220723_AddUsersAndRoles")] + partial class AddUsersAndRoles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.HasOne("DEMO.Models.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908220723_AddUsersAndRoles.cs b/DEMO/Migrations/20260908220723_AddUsersAndRoles.cs new file mode 100644 index 0000000..2358d8d --- /dev/null +++ b/DEMO/Migrations/20260908220723_AddUsersAndRoles.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddUsersAndRoles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Login = table.Column(type: "nvarchar(max)", nullable: false), + Password = table.Column(type: "nvarchar(max)", nullable: false), + RoleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_RoleId", + table: "Users", + column: "RoleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/DEMO/Migrations/20260908221344_AddOrders.Designer.cs b/DEMO/Migrations/20260908221344_AddOrders.Designer.cs new file mode 100644 index 0000000..6765868 --- /dev/null +++ b/DEMO/Migrations/20260908221344_AddOrders.Designer.cs @@ -0,0 +1,239 @@ +// +using System; +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908221344_AddOrders")] + partial class AddOrders + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.HasOne("DEMO.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.HasOne("DEMO.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908221344_AddOrders.cs b/DEMO/Migrations/20260908221344_AddOrders.cs new file mode 100644 index 0000000..d98c2af --- /dev/null +++ b/DEMO/Migrations/20260908221344_AddOrders.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddOrders : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "int", nullable: false), + Date = table.Column(type: "datetime2", nullable: false), + Status = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_UserId", + table: "Orders", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Orders"); + } + } +} diff --git a/DEMO/Migrations/20260908221738_AddOrderItems.Designer.cs b/DEMO/Migrations/20260908221738_AddOrderItems.Designer.cs new file mode 100644 index 0000000..f60e3ca --- /dev/null +++ b/DEMO/Migrations/20260908221738_AddOrderItems.Designer.cs @@ -0,0 +1,287 @@ +// +using System; +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260908221738_AddOrderItems")] + partial class AddOrderItems + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("OrderId") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.HasOne("DEMO.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.HasOne("DEMO.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.HasOne("DEMO.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260908221738_AddOrderItems.cs b/DEMO/Migrations/20260908221738_AddOrderItems.cs new file mode 100644 index 0000000..858a30b --- /dev/null +++ b/DEMO/Migrations/20260908221738_AddOrderItems.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddOrderItems : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + OrderId = table.Column(type: "int", nullable: false), + ProductId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_ProductId", + table: "OrderItems", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrderItems"); + } + } +} diff --git a/DEMO/Migrations/AppDbContextModelSnapshot.cs b/DEMO/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..1c3243c --- /dev/null +++ b/DEMO/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,284 @@ +// +using System; +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("OrderId") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.HasOne("DEMO.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.HasOne("DEMO.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.HasOne("DEMO.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Models/Category.cs b/DEMO/Models/Category.cs new file mode 100644 index 0000000..8190fe1 --- /dev/null +++ b/DEMO/Models/Category.cs @@ -0,0 +1,9 @@ +// категория товаров, например еда или одежда +namespace DEMO.Models; + +public class Category +{ + public int Id { get; set; } // номер + + public string Name { get; set; } = string.Empty; // название +} diff --git a/DEMO/Models/Order.cs b/DEMO/Models/Order.cs new file mode 100644 index 0000000..87d1f6c --- /dev/null +++ b/DEMO/Models/Order.cs @@ -0,0 +1,15 @@ +// заказ от покупателя +namespace DEMO.Models; + +public class Order +{ + public int Id { get; set; } // номер заказа + + public int UserId { get; set; } // кто заказал (номер человека) + + public DateTime Date { get; set; } // когда заказал + + public string Status { get; set; } = string.Empty; // статус: новый, готов и тд + + public User? User { get; set; } // сам человек (подтянется) +} diff --git a/DEMO/Models/OrderItem.cs b/DEMO/Models/OrderItem.cs new file mode 100644 index 0000000..2e7d8c2 --- /dev/null +++ b/DEMO/Models/OrderItem.cs @@ -0,0 +1,19 @@ +// один товар внутри заказа +namespace DEMO.Models; + +public class OrderItem +{ + public int Id { get; set; } // номер + + public int OrderId { get; set; } // к какому заказу относится + + public int ProductId { get; set; } // какой товар + + public int Quantity { get; set; } // сколько штук взяли + + public decimal Price { get; set; } // цена на момент покупки + + public Order? Order { get; set; } // сам заказ (подтянется) + + public Product? Product { get; set; } // сам товар (подтянется) +} diff --git a/DEMO/Models/Product.cs b/DEMO/Models/Product.cs new file mode 100644 index 0000000..9dc000f --- /dev/null +++ b/DEMO/Models/Product.cs @@ -0,0 +1,20 @@ +// товар в магазине +using System.ComponentModel.DataAnnotations; // проверки + +namespace DEMO.Models; + +public class Product +{ + public int Id { get; set; } // номер товара + + [Required] // название обязательно + [MinLength(2)] // минимум 2 буквы + public string Name { get; set; } = string.Empty; // название + + [Range(0.01, double.MaxValue)] // цена больше нуля + public decimal Price { get; set; } // цена + + public int CategoryId { get; set; } // к какой категории относится + + public Category? Category { get; set; } // сама категория (подтянется сама) +} diff --git a/DEMO/Models/Role.cs b/DEMO/Models/Role.cs new file mode 100644 index 0000000..a390e8a --- /dev/null +++ b/DEMO/Models/Role.cs @@ -0,0 +1,9 @@ +// роль человека, например админ или клиент +namespace DEMO.Models; + +public class Role +{ + public int Id { get; set; } // номер + + public string Name { get; set; } = string.Empty; // название роли +} diff --git a/DEMO/Models/Stock.cs b/DEMO/Models/Stock.cs new file mode 100644 index 0000000..78b0602 --- /dev/null +++ b/DEMO/Models/Stock.cs @@ -0,0 +1,17 @@ +// сколько какого товара лежит на складе +namespace DEMO.Models; + +public class Stock +{ + public int Id { get; set; } // номер записи + + public int ProductId { get; set; } // какой товар + + public int WarehouseId { get; set; } // какой склад + + public int Quantity { get; set; } // сколько штук + + public Product? Product { get; set; } // сам товар (подтянется) + + public Warehouse? Warehouse { get; set; } // сам склад (подтянется) +} diff --git a/DEMO/Models/User.cs b/DEMO/Models/User.cs new file mode 100644 index 0000000..6c286b0 --- /dev/null +++ b/DEMO/Models/User.cs @@ -0,0 +1,15 @@ +// человек в системе +namespace DEMO.Models; + +public class User +{ + public int Id { get; set; } // номер + + public string Login { get; set; } = string.Empty; // логин + + public string Password { get; set; } = string.Empty; // пароль + + public int RoleId { get; set; } // какая роль (номер) + + public Role? Role { get; set; } // сама роль (подтянется) +} diff --git a/DEMO/Models/Warehouse.cs b/DEMO/Models/Warehouse.cs new file mode 100644 index 0000000..893e934 --- /dev/null +++ b/DEMO/Models/Warehouse.cs @@ -0,0 +1,11 @@ +// склад где лежат товары +namespace DEMO.Models; + +public class Warehouse +{ + public int Id { get; set; } // номер + + public string Name { get; set; } = string.Empty; // название склада + + public string Address { get; set; } = string.Empty; // где находится +} diff --git a/DEMO/Program.cs b/DEMO/Program.cs new file mode 100644 index 0000000..8783c20 --- /dev/null +++ b/DEMO/Program.cs @@ -0,0 +1,34 @@ +// это главный файл, тут все начинается +using DEMO.Data; // база данных +using Microsoft.EntityFrameworkCore; // чтоб работать с базой + +var builder = WebApplication.CreateBuilder(args); + +// говорю что у меня есть контроллеры (api) +builder.Services.AddControllers(); + +// подключаю базу данных +builder.Services.AddDbContext(options => + options.UseSqlServer( + builder.Configuration.GetConnectionString("DefaultConnection"))); // беру строку из настроек + +builder.Services.AddOpenApi(); // для проверки api + +var app = builder.Build(); // строю программу + +// в режиме разработки показываю api +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); // всегда https + +app.UseDefaultFiles(); // чтоб открывался index.html сам +app.UseStaticFiles(); // чтоб отдавать файлы из wwwroot + +app.UseAuthorization(); // проверка прав (пока простая) + +app.MapControllers(); // включаю все контроллеры + +app.Run(); // запускаю, поехали diff --git a/DEMO/Properties/launchSettings.json b/DEMO/Properties/launchSettings.json new file mode 100644 index 0000000..230008c --- /dev/null +++ b/DEMO/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5214", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7184;http://localhost:5214", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DEMO/appsettings.Development.json b/DEMO/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/DEMO/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DEMO/appsettings.json b/DEMO/appsettings.json new file mode 100644 index 0000000..d68e734 --- /dev/null +++ b/DEMO/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=DemoDB;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DEMO/wwwroot/index.html b/DEMO/wwwroot/index.html new file mode 100644 index 0000000..756b233 --- /dev/null +++ b/DEMO/wwwroot/index.html @@ -0,0 +1,160 @@ + + + + + + DEMO Shop + + + + + + +
+
+ + + + + +
+
+

Вход

+

Войдите чтобы работать

+
+ +
+ + + + + +
+ + +
+ +
+ Нет аккаунта? + +
+
+ + + + +
+
+ + + + + + + + +
+ + + + + + diff --git a/DEMO/wwwroot/script.js b/DEMO/wwwroot/script.js new file mode 100644 index 0000000..3d03d28 --- /dev/null +++ b/DEMO/wwwroot/script.js @@ -0,0 +1,877 @@ +// нахожу экран входа и главное окно +const authScreen = document.getElementById("authScreen"); +const app = document.getElementById("app"); + +// нахожу формы входа и регистрации +const loginFormContainer = document.getElementById("loginFormContainer"); +const registerFormContainer = document.getElementById("registerFormContainer"); +const loginForm = document.getElementById("loginForm"); +const registerForm = document.getElementById("registerForm"); + +// сюда пишу ошибки +const loginError = document.getElementById("loginError"); +const registerError = document.getElementById("registerError"); + +// кнопки переключения вход / регистрация +const showRegisterButton = document.getElementById("showRegister"); +const showLoginButton = document.getElementById("showLogin"); + +// кнопка выйти +const logoutButton = document.getElementById("logoutButton"); + +// где показываю кто вошел +const currentUser = document.getElementById("currentUser"); +const currentRole = document.getElementById("currentRole"); +const userAvatar = document.getElementById("userAvatar"); + +// заголовок страницы и место для таблицы +const pageTitle = document.getElementById("pageTitle"); +const pageDescription = document.getElementById("pageDescription"); +const pageContent = document.getElementById("pageContent"); +const addButton = document.getElementById("addButton"); + +// всплывающее окно +const modal = document.getElementById("modal"); +const modalTitle = document.getElementById("modalTitle"); +const modalForm = document.getElementById("modalForm"); +const closeModal = document.getElementById("closeModal"); + +// маленькое сообщение снизу +const toast = document.getElementById("toast"); + +// список всех страниц и откуда брать данные +const pages = { + products: { title: "Товары", description: "Все товары магазина", endpoint: "/api/products" }, + categories: { title: "Категории", description: "Категории товаров", endpoint: "/api/categories" }, + warehouses: { title: "Склады", description: "Все склады", endpoint: "/api/warehouses" }, + stock: { title: "Остатки", description: "Что лежит на складах", endpoint: "/api/stocks" }, + users: { title: "Пользователи", description: "Все люди в системе", endpoint: "/api/users" }, + orders: { title: "Заказы", description: "Все заказы", endpoint: "/api/orders" } +}; + +// какая страница открыта сейчас +let currentPage = "products"; +// кого изменяю (если null значит добавляю нового) +let editingId = null; +// кто вошел в систему +let currentAuthUser = null; + +// показываю сообщение снизу на 2 секунды +function showToast(message) { + toast.textContent = message; // пишу текст + toast.classList.add("show"); // показываю + setTimeout(() => { + toast.classList.remove("show"); // прячу обратно + }, 2500); +} + +// защита от плохих символов чтоб не ломали страницу +function escapeHtml(value) { + if (value === null || value === undefined) { + return ""; + } + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +// делаю цену красивой: 1000 -> 1 000,00 ₽ +function formatMoney(value) { + return Number(value).toLocaleString("ru-RU", { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }) + " ₽"; +} + +// делаю дату понятной +function formatDate(value) { + if (!value) { + return ""; + } + return new Date(value).toLocaleString("ru-RU"); +} + +// одна функция для всех запросов на сервер +async function api(url, options = {}) { + // отправляю запрос + const response = await fetch(url, { + headers: { "Content-Type": "application/json" }, + ...options + }); + // если ошибка то показываю что случилось + if (!response.ok) { + let message = "Ошибка."; + const text = await response.text(); + if (text) { + try { + const data = JSON.parse(text); + message = data.title || data.message || text; + } catch { + message = text; + } + } + throw new Error(message); + } + // если пусто то возвращаю null + if (response.status === 204) { + return null; + } + const text = await response.text(); + if (!text) { + return null; + } + return JSON.parse(text); // превращаю ответ в объект +} + +// показываю форму входа +function showLoginForm() { + loginFormContainer.classList.remove("hidden"); + registerFormContainer.classList.add("hidden"); + loginError.textContent = ""; + registerError.textContent = ""; +} + +// показываю форму регистрации +function showRegisterForm() { + loginFormContainer.classList.add("hidden"); + registerFormContainer.classList.remove("hidden"); + loginError.textContent = ""; + registerError.textContent = ""; +} + +// захожу в программу после входа +function enterApplication(user) { + currentAuthUser = user; // запоминаю кто вошел + sessionStorage.setItem("demoUser", JSON.stringify(user)); // сохраняю чтоб не выкидывало + currentUser.textContent = user.login; // пишу логин + currentRole.textContent = user.role || "Client"; // пишу роль + userAvatar.textContent = user.login ? user.login.charAt(0).toUpperCase() : "U"; // первая буква в кружок + authScreen.classList.add("hidden"); // прячу вход + app.classList.remove("hidden"); // показываю программу + showPage("products"); // открываю товары +} + +// выхожу из аккаунта +function logout() { + sessionStorage.removeItem("demoUser"); // забываю пользователя + currentAuthUser = null; + app.classList.add("hidden"); // прячу программу + authScreen.classList.remove("hidden"); // показываю вход + loginForm.reset(); // чищу поля + registerForm.reset(); + showLoginForm(); +} + +// когда жму войти +loginForm.addEventListener("submit", async (event) => { + event.preventDefault(); // чтоб страница не перезагружалась + loginError.textContent = ""; + const login = document.getElementById("login").value.trim(); + const password = document.getElementById("password").value; + if (!login || !password) { + loginError.textContent = "Напишите логин и пароль."; + return; + } + try { + const user = await api("/api/auth/login", { + method: "POST", + body: JSON.stringify({ login, password }) + }); + enterApplication(user); // захожу + loginForm.reset(); + } catch (error) { + loginError.textContent = error.message || "Не получилось войти."; + } +}); + +// когда жму зарегистрироваться +registerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + registerError.textContent = ""; + const login = document.getElementById("registerLogin").value.trim(); + const password = document.getElementById("registerPassword").value; + const passwordRepeat = document.getElementById("registerPasswordRepeat").value; + // простые проверки + if (!login || !password || !passwordRepeat) { + registerError.textContent = "Заполните все поля."; + return; + } + if (login.length < 2) { + registerError.textContent = "Логин слишком короткий."; + return; + } + if (password.length < 4) { + registerError.textContent = "Пароль слишком короткий."; + return; + } + if (password !== passwordRepeat) { + registerError.textContent = "Пароли не совпадают."; + return; + } + try { + await api("/api/auth/register", { + method: "POST", + body: JSON.stringify({ login, password, roleId: 2 }) + }); + registerForm.reset(); + showLoginForm(); // иду на вход + document.getElementById("login").value = login; + loginError.textContent = "Готово. Теперь войдите."; + } catch (error) { + registerError.textContent = error.message || "Не получилось."; + } +}); + +// кнопки переключения +showRegisterButton.addEventListener("click", showRegisterForm); +showLoginButton.addEventListener("click", showLoginForm); +logoutButton.addEventListener("click", logout); + +// кнопки меню слева +document.querySelectorAll(".nav-button").forEach((button) => { + button.addEventListener("click", () => { + // убираю синий цвет у всех + document.querySelectorAll(".nav-button").forEach((b) => b.classList.remove("active")); + button.classList.add("active"); // крашу нажатую + showPage(button.dataset.page); // открываю страницу + }); +}); + +// открываю страницу (товары, заказы и тд) +async function showPage(page) { + currentPage = page; // запоминаю где я + const config = pages[page]; + if (!config) { + return; + } + pageTitle.textContent = config.title; // пишу название + pageDescription.textContent = config.description; + pageContent.innerHTML = "
Загрузка...
"; // пока грузится + try { + const data = await api(config.endpoint); // беру данные с сервера + renderPage(page, data); // рисую + } catch (error) { + pageContent.innerHTML = "
Не получилось: " + escapeHtml(error.message) + "
"; + } +} + +// выбираю что рисовать +function renderPage(page, data) { + // если пусто то так и пишу + if (!Array.isArray(data) || data.length === 0) { + pageContent.innerHTML = "
Пока пустоНажмите Добавить
"; + return; + } + if (page === "products") { + renderProducts(data); + } + if (page === "categories") { + renderCategories(data); + } + if (page === "warehouses") { + renderWarehouses(data); + } + if (page === "stock") { + renderStock(data); + } + if (page === "users") { + renderUsers(data); + } + if (page === "orders") { + renderOrders(data); + } +} + +// рисую товары карточками +function renderProducts(products) { + let html = "
"; // начало сетки + for (const p of products) { + html += "
"; + html += "
#" + p.id + "
"; + html += "

" + escapeHtml(p.name) + "

"; + html += "
" + formatMoney(p.price) + "
"; + html += "
Категория: " + escapeHtml(p.category ? p.category.name : "—") + "
"; + html += "
"; + html += ""; + html += ""; + html += "
"; + } + html += "
"; + pageContent.innerHTML = html; // показываю все +} + +// рисую категории таблицей +function renderCategories(categories) { + let html = "
"; + for (const c of categories) { + html += ""; + } + html += "
IDНазваниеДействия
#" + c.id + "" + escapeHtml(c.name) + "
"; + html += ""; + html += ""; + html += "
"; + pageContent.innerHTML = html; +} + +// рисую склады +function renderWarehouses(warehouses) { + let html = "
"; + for (const w of warehouses) { + html += ""; + } + html += "
IDНазваниеАдресДействия
#" + w.id + "" + escapeHtml(w.name) + "" + escapeHtml(w.address) + "
"; + html += ""; + html += ""; + html += "
"; + pageContent.innerHTML = html; +} + +// рисую остатки +function renderStock(stocks) { + let html = "
"; + for (const s of stocks) { + const pname = s.product ? s.product.name : "—"; + const wname = s.warehouse ? s.warehouse.name : "—"; + html += ""; + } + html += "
IDТоварСкладКол-воДействия
#" + s.id + "" + escapeHtml(pname) + "" + escapeHtml(wname) + "" + s.quantity + "
"; + html += ""; + html += ""; + html += "
"; + pageContent.innerHTML = html; +} + +// рисую пользователей +function renderUsers(users) { + let html = "
"; + for (const u of users) { + const rname = u.role ? u.role.name : "—"; + html += ""; + } + html += "
IDЛогинРольДействия
#" + u.id + "" + escapeHtml(u.login) + "" + escapeHtml(rname) + "
"; + html += ""; + html += ""; + html += "
"; + pageContent.innerHTML = html; +} + +// рисую заказы +function renderOrders(orders) { + let html = "
"; + for (const o of orders) { + const uname = o.user ? o.user.login : "—"; + html += ""; + } + html += "
IDКто заказалДатаСтатусДействия
#" + o.id + "" + escapeHtml(uname) + "" + formatDate(o.date) + "" + escapeHtml(o.status) + "
"; + html += ""; + html += ""; + html += ""; + html += "
"; + pageContent.innerHTML = html; +} + +// кнопка Добавить сверху +addButton.addEventListener("click", () => { + editingId = null; // значит добавляю а не изменяю + if (currentPage === "products") { + openProductForm(); + } + if (currentPage === "categories") { + openCategoryForm(); + } + if (currentPage === "warehouses") { + openWarehouseForm(); + } + if (currentPage === "stock") { + openStockForm(); + } + if (currentPage === "users") { + openUserForm(); + } + if (currentPage === "orders") { + openOrderForm(); + } +}); + +// открываю всплывающее окно +function openModal(title, content) { + modalTitle.textContent = title; // пишу заголовок + modalForm.innerHTML = content; // рисую поля + modal.classList.remove("hidden"); // показываю +} + +// закрываю окно +function closeModalWindow() { + modal.classList.add("hidden"); // прячу + modalForm.innerHTML = ""; // чищу + editingId = null; // забываю кого изменял +} + +// закрытие по крестику и по темному фону +closeModal.addEventListener("click", closeModalWindow); +document.querySelector(".modal-overlay").addEventListener("click", closeModalWindow); + +// форма товара +async function openProductForm(product = null) { + const categories = await api("/api/categories"); // беру категории для списка + let options = ""; + for (const c of categories) { + const sel = product && product.categoryId === c.id ? "selected" : ""; + options += ""; + } + const title = product ? "Изменить товар" : "Добавить товар"; + const name = product ? escapeHtml(product.name) : ""; + const price = product ? product.price : ""; + openModal(title, + "
" + + "" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю товар +async function saveProduct() { + const name = document.getElementById("productName").value.trim(); + const price = Number(document.getElementById("productPrice").value); + const categoryId = Number(document.getElementById("productCategory").value); + if (!name || name.length < 2) { + showToast("Название слишком короткое."); + return; + } + if (!price || price <= 0) { + showToast("Цена должна быть больше 0."); + return; + } + try { + const data = { name, price, categoryId }; + if (editingId) { + await api("/api/products/" + editingId, { method: "PUT", body: JSON.stringify(data) }); // изменяю + showToast("Товар изменен."); + } else { + await api("/api/products", { method: "POST", body: JSON.stringify(data) }); // добавляю + showToast("Товар добавлен."); + } + closeModalWindow(); + showPage("products"); // обновляю страницу + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на товаре +async function editProduct(id) { + const products = await api("/api/products"); // беру все + const product = products.find((x) => x.id === id); // ищу нужный + if (!product) { + return; + } + editingId = id; // запоминаю кого меняю + await openProductForm(product); +} + +// форма категории +function openCategoryForm(category = null) { + const title = category ? "Изменить категорию" : "Добавить категорию"; + const name = category ? escapeHtml(category.name) : ""; + openModal(title, + "
" + + "" + + "
" + + "
" + ); +} + +// сохраняю категорию +async function saveCategory() { + const name = document.getElementById("categoryName").value.trim(); + if (!name) { + showToast("Напишите название."); + return; + } + try { + if (editingId) { + await api("/api/categories/" + editingId, { method: "PUT", body: JSON.stringify({ name }) }); + showToast("Категория изменена."); + } else { + await api("/api/categories", { method: "POST", body: JSON.stringify({ name }) }); + showToast("Категория добавлена."); + } + closeModalWindow(); + showPage("categories"); + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на категории +async function editCategory(id) { + const categories = await api("/api/categories"); + const category = categories.find((x) => x.id === id); + if (!category) { + return; + } + editingId = id; + openCategoryForm(category); +} + +// форма склада +function openWarehouseForm(warehouse = null) { + const title = warehouse ? "Изменить склад" : "Добавить склад"; + const name = warehouse ? escapeHtml(warehouse.name) : ""; + const address = warehouse ? escapeHtml(warehouse.address) : ""; + openModal(title, + "
" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю склад +async function saveWarehouse() { + const name = document.getElementById("warehouseName").value.trim(); + const address = document.getElementById("warehouseAddress").value.trim(); + if (!name || !address) { + showToast("Заполните все поля."); + return; + } + try { + const data = { name, address }; + if (editingId) { + await api("/api/warehouses/" + editingId, { method: "PUT", body: JSON.stringify(data) }); + showToast("Склад изменен."); + } else { + await api("/api/warehouses", { method: "POST", body: JSON.stringify(data) }); + showToast("Склад добавлен."); + } + closeModalWindow(); + showPage("warehouses"); + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на складе +async function editWarehouse(id) { + const warehouses = await api("/api/warehouses"); + const warehouse = warehouses.find((x) => x.id === id); + if (!warehouse) { + return; + } + editingId = id; + openWarehouseForm(warehouse); +} + +// форма остатка +async function openStockForm(stock = null) { + const products = await api("/api/products"); // беру товары + const warehouses = await api("/api/warehouses"); // беру склады + let pOptions = ""; + for (const p of products) { + const sel = stock && stock.productId === p.id ? "selected" : ""; + pOptions += ""; + } + let wOptions = ""; + for (const w of warehouses) { + const sel = stock && stock.warehouseId === w.id ? "selected" : ""; + wOptions += ""; + } + const qty = stock ? stock.quantity : 0; + const title = stock ? "Изменить остаток" : "Добавить остаток"; + openModal(title, + "
" + + "" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю остаток +async function saveStock() { + const productId = Number(document.getElementById("stockProduct").value); + const warehouseId = Number(document.getElementById("stockWarehouse").value); + const quantity = Number(document.getElementById("stockQuantity").value); + if (quantity < 0) { + showToast("Количество не может быть минусом."); + return; + } + try { + const data = { productId, warehouseId, quantity }; + if (editingId) { + await api("/api/stocks/" + editingId, { method: "PUT", body: JSON.stringify(data) }); + showToast("Остаток изменен."); + } else { + await api("/api/stocks", { method: "POST", body: JSON.stringify(data) }); + showToast("Остаток добавлен."); + } + closeModalWindow(); + showPage("stock"); + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на остатке +async function editStock(id) { + const stocks = await api("/api/stocks"); + const stock = stocks.find((x) => x.id === id); + if (!stock) { + return; + } + editingId = id; + await openStockForm(stock); +} + +// форма пользователя +async function openUserForm(user = null) { + const roles = await api("/api/roles"); // беру роли + let options = ""; + for (const r of roles) { + const sel = user && user.roleId === r.id ? "selected" : ""; + options += ""; + } + const title = user ? "Изменить пользователя" : "Добавить пользователя"; + const login = user ? escapeHtml(user.login) : ""; + const password = user ? escapeHtml(user.password) : ""; + openModal(title, + "
" + + "" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю пользователя +async function saveUser() { + const login = document.getElementById("userLogin").value.trim(); + const password = document.getElementById("userPassword").value; + const roleId = Number(document.getElementById("userRole").value); + if (!login || !password) { + showToast("Заполните все поля."); + return; + } + try { + const data = { login, password, roleId }; + if (editingId) { + await api("/api/users/" + editingId, { method: "PUT", body: JSON.stringify(data) }); + showToast("Пользователь изменен."); + } else { + await api("/api/users", { method: "POST", body: JSON.stringify(data) }); + showToast("Пользователь добавлен."); + } + closeModalWindow(); + showPage("users"); + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на пользователе +async function editUser(id) { + const users = await api("/api/users"); + const user = users.find((x) => x.id === id); + if (!user) { + return; + } + editingId = id; + openUserForm(user); // тут без await специально, форма и так откроется +} + +// форма заказа +async function openOrderForm(order = null) { + const users = await api("/api/users"); // беру людей для списка + let options = ""; + for (const u of users) { + const sel = order && order.userId === u.id ? "selected" : ""; + options += ""; + } + // дата для поля (просто сегодня или дата заказа) + const dateValue = order ? new Date(order.date).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16); + const title = order ? "Изменить заказ" : "Добавить заказ"; + // какой статус выбран + const st = order ? order.status : "New"; + openModal(title, + "
" + + "" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю заказ +async function saveOrder() { + const userId = Number(document.getElementById("orderUser").value); + const date = document.getElementById("orderDate").value; + const status = document.getElementById("orderStatus").value; + if (!date) { + showToast("Выберите дату."); + return; + } + try { + const data = { userId, date: new Date(date).toISOString(), status }; + if (editingId) { + await api("/api/orders/" + editingId, { method: "PUT", body: JSON.stringify(data) }); + showToast("Заказ изменен."); + } else { + await api("/api/orders", { method: "POST", body: JSON.stringify(data) }); + showToast("Заказ добавлен."); + } + closeModalWindow(); + showPage("orders"); + } catch (error) { + showToast(error.message); + } +} + +// жму изменить на заказе +async function editOrder(id) { + const orders = await api("/api/orders"); + const order = orders.find((x) => x.id === id); + if (!order) { + return; + } + editingId = id; + await openOrderForm(order); +} + +// показываю товары внутри заказа +async function showOrderItems(orderId) { + try { + const items = await api("/api/orderitems"); // беру все позиции + const products = await api("/api/products"); // беру товары чтоб узнать названия + const orderItems = items.filter((i) => i.orderId === orderId); // оставляю только этот заказ + let html = "
"; + if (orderItems.length === 0) { + html += "
Тут пока пусто
"; // если ничего нет + } else { + for (const item of orderItems) { + const prod = products.find((x) => x.id === item.productId); // ищу название товара + const pname = prod ? prod.name : "Товар"; + html += "
" + escapeHtml(pname) + "Кол-во: " + item.quantity + "
"; + html += "" + formatMoney(item.price) + "
"; + } + } + html += "
"; + openModal("Товары заказа #" + orderId, html); + } catch (error) { + showToast(error.message); + } +} + +// форма добавить товар в заказ +async function openOrderItemForm(orderId) { + const products = await api("/api/products"); + let options = ""; + for (const p of products) { + options += ""; + } + openModal("Товар в заказ #" + orderId, + "
" + + "" + + "" + + "" + + "
" + + "
" + ); +} + +// сохраняю товар в заказ +async function saveOrderItem(orderId) { + const productId = Number(document.getElementById("orderItemProduct").value); + const quantity = Number(document.getElementById("orderItemQuantity").value); + const price = Number(document.getElementById("orderItemPrice").value); + if (quantity <= 0) { + showToast("Количество должно быть больше 0."); + return; + } + if (price <= 0) { + showToast("Цена должна быть больше 0."); + return; + } + try { + await api("/api/orderitems", { + method: "POST", + body: JSON.stringify({ orderId, productId, quantity, price }) + }); + showToast("Товар добавлен."); + showOrderItems(orderId); // обновляю список + } catch (error) { + showToast(error.message); + } +} + +// удаляю что угодно (товар, заказ и тд) +async function deleteItem(type, id) { + // красивые названия для вопроса + const names = { + products: "товар", + categories: "категорию", + warehouses: "склад", + stock: "остаток", + users: "пользователя", + orders: "заказ" + }; + // откуда удалять + const endpoints = { + products: "/api/products", + categories: "/api/categories", + warehouses: "/api/warehouses", + stock: "/api/stocks", + users: "/api/users", + orders: "/api/orders" + }; + const name = names[type] || "запись"; + if (!confirm("Удалить " + name + "?")) { // спрашиваю точно ли надо + return; + } + try { + await api(endpoints[type] + "/" + id, { method: "DELETE" }); // удаляю + showToast("Удалено."); + showPage(currentPage); // обновляю страницу + } catch (error) { + showToast(error.message); + } +} + +// при запуске смотрю входил ли уже +const savedUser = sessionStorage.getItem("demoUser"); +if (savedUser) { + try { + const user = JSON.parse(savedUser); + if (user && user.login) { + enterApplication(user); // да, уже входил, сразу захожу + } else { + app.classList.add("hidden"); + authScreen.classList.remove("hidden"); + showLoginForm(); + } + } catch { + sessionStorage.removeItem("demoUser"); + app.classList.add("hidden"); + authScreen.classList.remove("hidden"); + showLoginForm(); + } +} else { + // нет, показываю вход + app.classList.add("hidden"); + authScreen.classList.remove("hidden"); + showLoginForm(); +} diff --git a/DEMO/wwwroot/style.css b/DEMO/wwwroot/style.css new file mode 100644 index 0000000..0c4236e --- /dev/null +++ b/DEMO/wwwroot/style.css @@ -0,0 +1,496 @@ +/* это самые простые стили, чтоб все было чисто и понятно */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #f2f2f2; /* серый фон */ + color: #222; /* темный текст */ + font-family: Arial, sans-serif; /* простой шрифт */ +} + +button, input, select { + font: inherit; /* шрифт как у всех */ +} + +button { + cursor: pointer; /* рука при наведении */ +} + +/* прячу то что не надо показывать */ +.hidden { + display: none !important; +} + +/* экран входа */ +.auth-screen { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +/* белая карточка входа */ +.auth-card { + width: 380px; + max-width: 100%; + padding: 30px; + background: white; + border: 1px solid #ddd; + border-radius: 12px; +} + +/* большой текст DEMO */ +.auth-logo { + text-align: center; + margin-bottom: 20px; + font-size: 28px; + font-weight: bold; +} + +.auth-logo small { + display: block; + font-size: 12px; + color: #888; + font-weight: normal; +} + +/* заголовок входа */ +.auth-heading { + margin-bottom: 15px; +} + +.auth-heading p { + color: #666; + font-size: 14px; +} + +/* форма входа */ +.auth-form { + display: flex; + flex-direction: column; + gap: 12px; /* отступы между полями */ +} + +.auth-form label { + display: flex; + flex-direction: column; + gap: 5px; + font-size: 14px; +} + +.auth-form input { + height: 40px; + padding: 0 10px; + border: 1px solid #ccc; + border-radius: 8px; +} + +/* синяя кнопка */ +.auth-button { + height: 42px; + border: 0; + border-radius: 8px; + background: #3366ff; + color: white; + font-weight: bold; +} + +/* ссылка регистрация */ +.auth-switch { + margin-top: 15px; + text-align: center; + font-size: 13px; + color: #666; +} + +.auth-switch button { + border: 0; + background: none; + color: #3366ff; +} + +/* красный текст ошибки */ +.auth-error { + min-height: 18px; + color: red; + font-size: 13px; +} + +/* главное окно */ +.app { + display: flex; + min-height: 100vh; +} + +/* меню слева */ +.sidebar { + width: 220px; + padding: 20px 12px; + background: white; + border-right: 1px solid #ddd; + display: flex; + flex-direction: column; + gap: 5px; +} + +/* логотип */ +.logo { + font-size: 22px; + font-weight: bold; + padding: 0 8px; +} + +.logo span { + display: block; + font-size: 11px; + color: #888; + font-weight: normal; +} + +/* кто вошел */ +.user-panel { + display: flex; + gap: 10px; + align-items: center; + margin: 20px 0; + padding: 10px; + background: #f2f2f2; + border-radius: 8px; +} + +/* кружок с буквой */ +.user-avatar { + width: 35px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + background: #3366ff; + color: white; + font-weight: bold; + border-radius: 8px; +} + +.user-info strong { + display: block; + font-size: 13px; +} + +.user-info span { + font-size: 12px; + color: #888; +} + +/* кнопки меню */ +.nav-button { + width: 100%; + text-align: left; + padding: 10px; + border: 0; + border-radius: 8px; + background: none; + color: #444; +} + +/* активная кнопка меню */ +.nav-button.active { + background: #e6ecff; + color: #2244cc; + font-weight: bold; +} + +/* кнопка выйти */ +.logout-button { + margin-top: auto; /* прижимаю вниз */ + padding: 10px; + border: 0; + border-radius: 8px; + background: none; + color: #666; + text-align: left; +} + +/* правая часть */ +.content { + flex: 1; + padding: 30px; +} + +/* верх с названием */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.header p { + color: #666; + font-size: 14px; + margin-top: 5px; +} + +/* синяя кнопка добавить */ +.primary-button { + padding: 10px 16px; + border: 0; + border-radius: 8px; + background: #3366ff; + color: white; + font-weight: bold; +} + +/* сетка товаров */ +.cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; +} + +/* одна карточка товара */ +.card { + background: white; + border: 1px solid #ddd; + border-radius: 10px; + padding: 15px; +} + +.card h3 { + font-size: 16px; + margin: 5px 0; +} + +.card-id { + font-size: 12px; + color: #888; +} + +.card-price { + font-size: 20px; + font-weight: bold; + margin: 10px 0; +} + +.card-info { + font-size: 13px; + color: #555; +} + +.card-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +/* маленькие кнопки */ +.action-button { + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 6px; + background: white; + font-size: 12px; +} + +/* кнопка удалить красная */ +.action-button.delete { + color: red; + border-color: #ffaaaa; +} + +/* таблица */ +.table-wrapper { + background: white; + border: 1px solid #ddd; + border-radius: 10px; + overflow-x: auto; /* чтоб на телефоне листалась */ +} + +table { + width: 100%; + border-collapse: collapse; /* убираю двойные линии */ +} + +th { + text-align: left; + padding: 10px; + background: #f5f5f5; + font-size: 12px; + color: #666; +} + +td { + padding: 10px; + border-top: 1px solid #eee; + font-size: 14px; +} + +.table-actions { + display: flex; + gap: 6px; +} + +/* плашка статуса */ +.status { + background: #e6ecff; + padding: 3px 8px; + border-radius: 6px; + font-size: 12px; +} + +/* когда нет данных */ +.empty-page { + background: white; + border: 1px dashed #ccc; + border-radius: 10px; + padding: 40px; + text-align: center; + color: #888; +} + +/* когда ошибка */ +.error-page { + background: #ffe6e6; + border: 1px solid #ffaaaa; + border-radius: 10px; + padding: 20px; + color: red; +} + +/* темное окно формы */ +.modal { + position: fixed; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +/* затемнение сзади */ +.modal-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); +} + +/* белая форма */ +.modal-content { + position: relative; + width: 450px; + max-width: 100%; + background: white; + border-radius: 12px; + padding: 20px; +} + +/* верх формы */ +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +/* кнопка X */ +.close-button { + width: 30px; + height: 30px; + border: 1px solid #ccc; + border-radius: 6px; + background: white; +} + +/* поля формы */ +.form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.form label { + display: flex; + flex-direction: column; + gap: 5px; + font-size: 13px; +} + +.form input, .form select { + height: 38px; + padding: 0 10px; + border: 1px solid #ccc; + border-radius: 6px; +} + +/* кнопки сохранить отмена */ +.form-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.secondary-button { + padding: 8px 14px; + border: 1px solid #ccc; + border-radius: 6px; + background: white; +} + +/* товары внутри заказа */ +.order-item { + display: flex; + justify-content: space-between; + padding: 10px; + border: 1px solid #eee; + border-radius: 8px; + margin-bottom: 8px; +} + +.order-item-info { + display: flex; + flex-direction: column; + gap: 3px; + font-size: 13px; +} + +/* сообщение снизу */ +.toast { + position: fixed; + right: 20px; + bottom: 20px; + background: #333; + color: white; + padding: 12px 16px; + border-radius: 8px; + opacity: 0; /* прячу */ +} + +/* показываю сообщение */ +.toast.show { + opacity: 1; +} + +/* для телефона */ +@media (max-width: 800px) { + .app { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .content { + padding: 15px; + } + + .header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } +}