diff --git a/ChudoObuv.slnx b/ChudoObuv.slnx new file mode 100644 index 0000000..63afa36 --- /dev/null +++ b/ChudoObuv.slnx @@ -0,0 +1,3 @@ + + + diff --git a/ChudoObuv/App.xaml b/ChudoObuv/App.xaml new file mode 100644 index 0000000..b7fc699 --- /dev/null +++ b/ChudoObuv/App.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ChudoObuv/App.xaml.cs b/ChudoObuv/App.xaml.cs new file mode 100644 index 0000000..65deaf6 --- /dev/null +++ b/ChudoObuv/App.xaml.cs @@ -0,0 +1,32 @@ +using System.Configuration; +using System.Data; +using System.Windows; +using System.Globalization; +using System.Windows.Markup; +using System.Windows.Threading; +using ChudoObuv.Logic; + +namespace ChudoObuv +{ + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + // Русский формат чисел, цен и дат во всем приложении (в том числе в привязках XAML) + CultureInfo russian = new CultureInfo("ru-RU"); + CultureInfo.DefaultThreadCurrentCulture = russian; + CultureInfo.DefaultThreadCurrentUICulture = russian; + Thread.CurrentThread.CurrentCulture = russian; + Thread.CurrentThread.CurrentUICulture = russian; + FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), + new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(russian.IetfLanguageTag))); + base.OnStartup(e); + } + // Последняя защита от аварийного завершения: любая необработанная ошибка показывается пользователю + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + Messages.ShowError($"Произошла непредвиденная ошибка: {e.Exception.Message}\nПовторите действие.Если ошибка повторяется, перезапустите приложение."); + e.Handled = true; + } + } +} \ No newline at end of file diff --git a/ChudoObuv/AssemblyInfo.cs b/ChudoObuv/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/ChudoObuv/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/ChudoObuv/ChudoObuv.csproj b/ChudoObuv/ChudoObuv.csproj new file mode 100644 index 0000000..35714f5 --- /dev/null +++ b/ChudoObuv/ChudoObuv.csproj @@ -0,0 +1,24 @@ + + + + WinExe + net8.0-windows + disable + enable + true + Resources\app.ico + Чудо Обувь + Чудо Обувь + Чудо Обувь + + + + + + + + + + + + \ No newline at end of file diff --git a/ChudoObuv/Data/DataService.cs b/ChudoObuv/Data/DataService.cs new file mode 100644 index 0000000..785e432 --- /dev/null +++ b/ChudoObuv/Data/DataService.cs @@ -0,0 +1,321 @@ +using ChudoObuv.Logic; +using ChudoObuv.Models; +using MySqlConnector; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace ChudoObuv.Data +{ + +/// +/// Все запросы приложения к базе данных. +/// +public static class DataService + { + // ---------------------------- Пользователи ---------------------------- + public static User FindUserByLogin(string login) + { + const string sql = @" +SELECT u.user_id, u.last_name, u.first_name, u.patronymic, u.login, r.role_name +FROM users u +JOIN roles r ON r.role_id = u.role_id +WHERE u.login = @login"; + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@login", login); + using MySqlDataReader reader = command.ExecuteReader(); + return reader.Read() ? ReadUser(reader) : null; + } + public static List GetClients() + { + const string sql = @" +SELECT u.user_id, u.last_name, u.first_name, u.patronymic, u.login, r.role_name +FROM users u +JOIN roles r ON r.role_id = u.role_id +WHERE r.role_name = @role +ORDER BY u.last_name, u.first_name"; + List clients = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@role", User.ClientRole); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + clients.Add(ReadUser(reader)); + } + return clients; + } + private static User ReadUser(MySqlDataReader reader) + { + return new User + { + Id = reader.GetInt32("user_id"), + LastName = reader.GetString("last_name"), + FirstName = reader.GetString("first_name"), + Patronymic = reader.IsDBNull(reader.GetOrdinal("patronymic")) ? "" : reader.GetString("patronymic"), + Login = reader.GetString("login"), + Role = reader.GetString("role_name") + }; + } + // ------------------------------- Товары ------------------------------- + public static List GetProducts() + { + const string sql = @" +SELECT p.product_id, p.product_name, c.category_name, s.subcategory_name,m.manufacture_name,p.description, p.composition, p.price, p.image_file, + +COALESCE((SELECT SUM(si.quantity) FROM stock_items si WHERE si.product_id = p.product_id), 0) AS total_quantity, +EXISTS (SELECT 1 FROM order_items oi + +JOIN orders o ON o.order_id = oi.order_id +JOIN stock_items si ON si.stock_item_id = oi.stock_item_id +WHERE si.product_id = p.product_id +AND o.order_date >= @previousMonthStart + +AND o.order_date < @currentMonthStart) AS has_previous_month_orders + +FROM products p +JOIN categories c ON c.category_id = p.category_id +JOIN subcategories s ON s.subcategory_id = p.subcategory_id +JOIN manufacturers m ON m.manufacture_id = p.manufacturer_id + +ORDER BY p.product_id"; + DateTime today = DateTime.Today; + List products = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@previousMonthStart", PriceCalculator.GetPreviousMonthStart(today)); + command.Parameters.AddWithValue("@currentMonthStart", PriceCalculator.GetCurrentMonthStart(today)); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + products.Add(new Product + { + Id = reader.GetInt32("product_id"), + Name = reader.GetString("product_name"), + Category = reader.GetString("category_name"), + Subcategory = reader.GetString("subcategory_name"), + Manufacturer = reader.GetString("manufacture_name"), + Description = reader.IsDBNull(reader.GetOrdinal("description")) ? "" : + + reader.GetString("description"), + Composition = reader.IsDBNull(reader.GetOrdinal("composition")) ? "" : + reader.GetString("composition"), + Price = reader.GetDecimal("price"), + + ImageFile = reader.IsDBNull(reader.GetOrdinal("image_file")) ? null : + + reader.GetString("image_file"), + TotalQuantity = Convert.ToInt32(reader["total_quantity"]), + + HasOrdersInPreviousMonth = Convert.ToInt32(reader["has_previous_month_orders"]) == 1 + + }); + } + return products; + } + public static List GetCategories() + { + List categories = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand("SELECT category_name FROM categories ORDER BY category_name", connection); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + categories.Add(reader.GetString(0)); + } + return categories; + } + /// + /// Все товарные позиции модели (размерный ряд) с доступным количеством. + /// + public static List GetStockItems(int productId) + { + const string sql = @" +SELECT si.stock_item_id, si.product_id, s.size_value, si.quantity +FROM stock_items si +JOIN sizes s ON s.size_id = si.size_id +WHERE si.product_id = @productId +ORDER BY s.size_value"; + List items = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@productId", productId); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + items.Add(new StockItem + { + Id = reader.GetInt32("stock_item_id"), + ProductId = reader.GetInt32("product_id"), + Size = reader.GetDecimal("size_value"), + Quantity = reader.GetInt32("quantity") + + }); + } + return items; + } +// ------------------------------- Заказы ------------------------------- + +public static List GetOrders() + { + const string sql = @" +SELECT o.order_id, o.order_date, u.user_id, +CONCAT_WS(' ', u.last_name, u.first_name, u.patronymic) AS client_name, + +COUNT(oi.order_item_id) AS items_count, +COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS total_sum + +FROM orders o +JOIN users u ON u.user_id = o.user_id +LEFT JOIN order_items oi ON oi.order_id = o.order_id +GROUP BY o.order_id, o.order_date, u.user_id, client_name +ORDER BY o.order_date DESC, o.order_id DESC"; + List orders = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + orders.Add(new Order + { + Id = reader.GetInt32("order_id"), + + OrderDate = reader.GetDateTime("order_date"), + ClientId = reader.GetInt32("user_id"), + ClientFullName = reader.GetString("client_name"), + ItemsCount = Convert.ToInt32(reader["items_count"]), + TotalSum = reader.GetDecimal("total_sum") + + }); + } + return orders; + } + public static List GetOrderItems(int orderId) + { + const string sql = @" +SELECT oi.order_item_id, oi.stock_item_id, p.product_name, m.manufacturer_name, s.size_value, oi.quantity, oi.unit_price +FROM order_items oi +JOIN stock_items si ON si.stock_item_id = oi.stock_item_id +JOIN products p ON p.product_id = si.product_id +JOIN manufacturers m ON m.manufacturer_id = p.manufacturer_id +JOIN sizes s ON s.size_id = si.size_id +WHERE oi.order_id = @orderId +ORDER BY oi.order_item_id"; + List items = new List(); + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@orderId", orderId); + using MySqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + items.Add(new OrderItem + { + Id = reader.GetInt32("order_item_id"), + StockItemId = reader.GetInt32("stock_item_id"), + ProductName = reader.GetString("product_name"), + Manufacturer = reader.GetString("manufacturer_name"), + Size = reader.GetDecimal("size_value"), + Quantity = reader.GetInt32("quantity"), + UnitPrice = reader.GetDecimal("unit_price") + + }); + } + return items; + } + /// + /// Сохраняет заказ и уменьшает остатки. Возвращает номер заказа. + /// Если какого-то размера уже не хватает — заказ не сохраняется (исключение). + /// + public static int CreateOrder(int clientId, List items) + { + using MySqlConnection connection = Database.OpenConnection(); + using MySqlTransaction transaction = connection.BeginTransaction(); + MySqlCommand orderCommand = new MySqlCommand( + + "INSERT INTO orders (order_date, user_id) VALUES (@date, @userId)", connection, + transaction); + orderCommand.Parameters.AddWithValue("@date", DateTime.Today); + orderCommand.Parameters.AddWithValue("@userId", clientId); + orderCommand.ExecuteNonQuery(); + int orderId = (int)orderCommand.LastInsertedId; + foreach (OrderItem item in items) + { + // Остаток уменьшается только если его хватает — защита от одновременных заказов + MySqlCommand stockCommand = new MySqlCommand( + "UPDATE stock_items SET quantity = quantity - @quantity WHERE stock_item_id = @id AND quantity >= @quantity", + connection, transaction); + stockCommand.Parameters.AddWithValue("@quantity", item.Quantity); + stockCommand.Parameters.AddWithValue("@id", item.StockItemId); + if (stockCommand.ExecuteNonQuery() == 0) + { + transaction.Rollback(); + + throw new InvalidOperationException( + + $"Недостаточно товара «{item.ProductName}», размер {item.SizeText}. Обновите каталог и измените количество."); + } + MySqlCommand itemCommand = new MySqlCommand( + "INSERT INTO order_items (order_id, stock_item_id, quantity, unit_price) VALUES (@orderId, @stockItemId, @quantity, @price)", + connection, transaction); + itemCommand.Parameters.AddWithValue("@orderId", orderId); + itemCommand.Parameters.AddWithValue("@stockItemId", item.StockItemId); + itemCommand.Parameters.AddWithValue("@quantity", item.Quantity); + itemCommand.Parameters.AddWithValue("@price", item.UnitPrice); + itemCommand.ExecuteNonQuery(); + } + transaction.Commit(); + return orderId; + } + /// + /// Удаляет заказ и возвращает его товары на склад. + /// + public static void DeleteOrder(int orderId) + { + using MySqlConnection connection = Database.OpenConnection(); + using MySqlTransaction transaction = connection.BeginTransaction(); + MySqlCommand returnCommand = new MySqlCommand(@" +UPDATE stock_items si +JOIN order_items oi ON oi.stock_item_id = si.stock_item_id +SET si.quantity = si.quantity + oi.quantity +WHERE oi.order_id = @orderId", connection, transaction); + returnCommand.Parameters.AddWithValue("@orderId", orderId); + returnCommand.ExecuteNonQuery(); + // Строки состава удаляются каскадно (ON DELETE CASCADE) + MySqlCommand deleteCommand = new MySqlCommand("DELETE FROM orders WHERE order_id = @orderId", connection, transaction); + deleteCommand.Parameters.AddWithValue("@orderId", orderId); + deleteCommand.ExecuteNonQuery(); + transaction.Commit(); + } + /// + /// Удаляет товарную позицию из заказа и возвращает ее количество на склад. + /// + public static void DeleteOrderItem(int orderItemId) + { + using MySqlConnection connection = Database.OpenConnection(); + using MySqlTransaction transaction = connection.BeginTransaction(); + MySqlCommand returnCommand = new MySqlCommand(@" +UPDATE stock_items si +JOIN order_items oi ON oi.stock_item_id = si.stock_item_id +SET si.quantity = si.quantity + oi.quantity +WHERE oi.order_item_id = @id", connection, transaction); + returnCommand.Parameters.AddWithValue("@id", orderItemId); + returnCommand.ExecuteNonQuery(); + MySqlCommand deleteCommand = new MySqlCommand("DELETE FROM order_items WHERE order_item_id = @id", connection, transaction); + deleteCommand.Parameters.AddWithValue("@id", orderItemId); + deleteCommand.ExecuteNonQuery(); + transaction.Commit(); + } + public static void UpdateOrderDate(int orderId, DateTime orderDate) + { + using MySqlConnection connection = Database.OpenConnection(); + using MySqlCommand command = new MySqlCommand("UPDATE orders SET order_date = @date WHERE order_id = @orderId", connection); + command.Parameters.AddWithValue("@date", orderDate.Date); + command.Parameters.AddWithValue("@orderId", orderId); + command.ExecuteNonQuery(); + } + } +} \ No newline at end of file diff --git a/ChudoObuv/Data/Database.cs b/ChudoObuv/Data/Database.cs new file mode 100644 index 0000000..ad82d44 --- /dev/null +++ b/ChudoObuv/Data/Database.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MySqlConnector; + +namespace ChudoObuv.Data +{ + /// + /// Подключение к базе данных MySQL. + /// + public static class Database + { + // Перед запуском укажите свои сервер, имя базы, пользователя и пароль MySQL + private const string ConnectionString = "Server=177.1.181.207;Port=3306;Database=isp-41_lipatova_yuv_chudo_obuv;UserID=isp-41_lipatova_yuv;Password=isp-41_lipatova_yuv;CharSet=utf8mb4;"; + public static MySqlConnection OpenConnection() + { + MySqlConnection connection = new MySqlConnection(ConnectionString); + connection.Open(); + return connection; + } + } +} \ No newline at end of file diff --git a/ChudoObuv/Logic/Messages.cs b/ChudoObuv/Logic/Messages.cs new file mode 100644 index 0000000..a3d1e45 --- /dev/null +++ b/ChudoObuv/Logic/Messages.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using MySqlConnector; + +namespace ChudoObuv.Logic +{ + /// + /// Окна сообщений с заголовком и пиктограммой нужного типа. + /// + public static class Messages + + +{ +private const string AppName = "Чудо Обувь"; + public static void ShowError(string text) + { + MessageBox.Show(text, $"{AppName} — Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); + } + public static void ShowWarning(string text) + { + MessageBox.Show(text, $"{AppName} — Предупреждение", MessageBoxButton.OK, + MessageBoxImage.Warning); + } + public static void ShowInfo(string text) + { + MessageBox.Show(text, $"{AppName} — Информация", MessageBoxButton.OK, + MessageBoxImage.Information); + } + /// + /// Подтверждение необратимой операции. Возвращает true, если пользователь нажал «Да». + /// + public static bool Confirm(string text) + { + MessageBoxResult result = MessageBox.Show(text, $"{AppName} — Подтверждение", + MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No); + return result == MessageBoxResult.Yes; + } + public static void ShowDatabaseError(Exception exception) + { + if (exception is MySqlException) + { + ShowError("Не удалось выполнить операцию с базой данных.\n" + + $"Причина: {exception.Message}\n\n" + + "Проверьте, что сервер MySQL запущен, а в файле Data\\Database.cs указаны верные сервер, пользователь и пароль."); + } + else + { + ShowError(exception.Message); + } + } +} +} \ No newline at end of file diff --git a/ChudoObuv/Logic/PriceCalculator.cs b/ChudoObuv/Logic/PriceCalculator.cs new file mode 100644 index 0000000..df8f533 --- /dev/null +++ b/ChudoObuv/Logic/PriceCalculator.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ChudoObuv.Logic +{ + /// + /// Алгоритмы расчета показателей товара. + /// + public static class PriceCalculator + { + public const decimal DiscountPercent = 25m; + public const int ManyQuantityThreshold = 5; + public const int LowStockThreshold = 3; + /// + /// Начало предыдущего календарного месяца относительно даты расчета. + /// + public static DateTime GetPreviousMonthStart(DateTime calculationDate) + { + return GetCurrentMonthStart(calculationDate).AddMonths(-1); + } + public static DateTime GetCurrentMonthStart(DateTime calculationDate) + { + return new DateTime(calculationDate.Year, calculationDate.Month, 1); + } + /// + /// Цена со скидкой 25%, если в предыдущем календарном месяце заказов товара не было. + /// + public static decimal CalculateDiscountedPrice(decimal price, bool hasOrdersInPreviousMonth) + { + if (hasOrdersInPreviousMonth) + { + return price; + } + return Math.Round(price * (100m - DiscountPercent) / 100m, 2); + } + /// + /// «много», если пар больше пяти, иначе «мало». + /// + public static string GetQuantityLabel(int totalQuantity) + { + return totalQuantity > ManyQuantityThreshold ? "много" : "мало"; + } + public static bool IsLowStock(int totalQuantity) + { + return totalQuantity <= LowStockThreshold; + } + } +} \ No newline at end of file diff --git a/ChudoObuv/Logic/Session.cs b/ChudoObuv/Logic/Session.cs new file mode 100644 index 0000000..e25a9e6 --- /dev/null +++ b/ChudoObuv/Logic/Session.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using ChudoObuv.Models; +namespace ChudoObuv.Logic +{ + /// + /// Текущий пользователь и формируемый (еще не подтвержденный) заказ. + /// + public static class Session + { + public static User CurrentUser { get; private set; } + /// + /// Клиент, для которого формируется заказ: сам пользователь или клиент, выбранный менеджером. + /// + public static User OrderClient { get; private set; } + public static List Cart { get; } = new List(); + public static bool IsAuthorized => CurrentUser != null; + public static void Login(User user) + { + CurrentUser = user; + OrderClient = user; + Cart.Clear(); + } + public static void Logout() + { + CurrentUser = null; + OrderClient = null; + Cart.Clear(); + } + public static void StartOrderFor(User client) + { + OrderClient = client; + Cart.Clear(); + } + public static void CancelOrder() + { + Cart.Clear(); + OrderClient = CurrentUser; + } + /// + /// Сколько пар данной товарной позиции уже лежит в формируемом заказе. + /// + public static int GetQuantityInCart(int stockItemId) + { + return Cart.Where(item => item.StockItemId == stockItemId).Sum(item => item.Quantity); + } + public static void AddToCart(OrderItem newItem) + { + OrderItem existing = Cart.FirstOrDefault(item => item.StockItemId == newItem.StockItemId); + if (existing != null) + { + existing.Quantity += newItem.Quantity; + } + else + { + Cart.Add(newItem); + } + } + +} +} \ No newline at end of file diff --git a/ChudoObuv/MainWindow.xaml b/ChudoObuv/MainWindow.xaml new file mode 100644 index 0000000..b8f153f --- /dev/null +++ b/ChudoObuv/MainWindow.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + +