Добавьте файлы проекта.
3
ChudoObuv.slnx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="ChudoObuv/ChudoObuv.csproj" />
|
||||||
|
</Solution>
|
||||||
57
ChudoObuv/App.xaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<Application x:Class="ChudoObuv.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml"
|
||||||
|
DispatcherUnhandledException="OnDispatcherUnhandledException">
|
||||||
|
<Application.Resources>
|
||||||
|
<!-- Цвета из руководства по стилю -->
|
||||||
|
<SolidColorBrush x:Key="MainBackgroundBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#D2F6E7" />
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#70B2AF" />
|
||||||
|
<SolidColorBrush x:Key="LowStockBrush" Color="#FF8080" />
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="FontFamily" Value="Calibri" />
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource SecondaryBackgroundBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="Padding" Value="12,4" />
|
||||||
|
<Setter Property="Margin" Value="4" />
|
||||||
|
<Setter Property="MinHeight" Value="32" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
</Style>
|
||||||
|
<!-- Кнопка целевого действия (акцентный цвет) -->
|
||||||
|
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="Calibri" />
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="Padding" Value="4,2" />
|
||||||
|
<Setter Property="Margin" Value="4" />
|
||||||
|
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="MinHeight" Value="30" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="FontFamily" Value="Calibri" />
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="Margin" Value="4" />
|
||||||
|
<Setter Property="MinHeight" Value="30" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="DataGrid">
|
||||||
|
<Setter Property="FontFamily" Value="Calibri" />
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||||
|
<Setter Property="IsReadOnly" Value="True" />
|
||||||
|
<Setter Property="CanUserAddRows" Value="False" />
|
||||||
|
<Setter Property="SelectionMode" Value="Single" />
|
||||||
|
<Setter Property="HeadersVisibility" Value="Column" />
|
||||||
|
<Setter Property="AlternatingRowBackground" Value="{StaticResource
|
||||||
|
SecondaryBackgroundBrush}" />
|
||||||
|
<Setter Property="Margin" Value="4" />
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
32
ChudoObuv/App.xaml.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ChudoObuv/AssemblyInfo.cs
Normal file
@ -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)
|
||||||
|
)]
|
||||||
24
ChudoObuv/ChudoObuv.csproj
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
|
||||||
|
<AssemblyTitle>Чудо Обувь</AssemblyTitle>
|
||||||
|
<Product>Чудо Обувь</Product>
|
||||||
|
<Company>Чудо Обувь</Company>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Resources\app.ico" />
|
||||||
|
<Resource Include="Resources\logo.png" />
|
||||||
|
<Content Include="Resources\picture.png" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="Resources\Images\*.png" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
321
ChudoObuv/Data/DataService.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Все запросы приложения к базе данных.
|
||||||
|
/// </summary>
|
||||||
|
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<User> 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<User> clients = new List<User>();
|
||||||
|
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<Product> 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<Product> products = new List<Product>();
|
||||||
|
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<string> GetCategories()
|
||||||
|
{
|
||||||
|
List<string> categories = new List<string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Все товарные позиции модели (размерный ряд) с доступным количеством.
|
||||||
|
/// </summary>
|
||||||
|
public static List<StockItem> 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<StockItem> items = new List<StockItem>();
|
||||||
|
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<Order> 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<Order> orders = new List<Order>();
|
||||||
|
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<OrderItem> 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<OrderItem> items = new List<OrderItem>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет заказ и уменьшает остатки. Возвращает номер заказа.
|
||||||
|
/// Если какого-то размера уже не хватает — заказ не сохраняется (исключение).
|
||||||
|
/// </summary>
|
||||||
|
public static int CreateOrder(int clientId, List<OrderItem> 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;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет заказ и возвращает его товары на склад.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет товарную позицию из заказа и возвращает ее количество на склад.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
ChudoObuv/Data/Database.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Подключение к базе данных MySQL.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
ChudoObuv/Logic/Messages.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Окна сообщений с заголовком и пиктограммой нужного типа.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Подтверждение необратимой операции. Возвращает true, если пользователь нажал «Да».
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
ChudoObuv/Logic/PriceCalculator.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Logic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Алгоритмы расчета показателей товара.
|
||||||
|
/// </summary>
|
||||||
|
public static class PriceCalculator
|
||||||
|
{
|
||||||
|
public const decimal DiscountPercent = 25m;
|
||||||
|
public const int ManyQuantityThreshold = 5;
|
||||||
|
public const int LowStockThreshold = 3;
|
||||||
|
/// <summary>
|
||||||
|
/// Начало предыдущего календарного месяца относительно даты расчета.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Цена со скидкой 25%, если в предыдущем календарном месяце заказов товара не было.
|
||||||
|
/// </summary>
|
||||||
|
public static decimal CalculateDiscountedPrice(decimal price, bool hasOrdersInPreviousMonth)
|
||||||
|
{
|
||||||
|
if (hasOrdersInPreviousMonth)
|
||||||
|
{
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
return Math.Round(price * (100m - DiscountPercent) / 100m, 2);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// «много», если пар больше пяти, иначе «мало».
|
||||||
|
/// </summary>
|
||||||
|
public static string GetQuantityLabel(int totalQuantity)
|
||||||
|
{
|
||||||
|
return totalQuantity > ManyQuantityThreshold ? "много" : "мало";
|
||||||
|
}
|
||||||
|
public static bool IsLowStock(int totalQuantity)
|
||||||
|
{
|
||||||
|
return totalQuantity <= LowStockThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
ChudoObuv/Logic/Session.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Текущий пользователь и формируемый (еще не подтвержденный) заказ.
|
||||||
|
/// </summary>
|
||||||
|
public static class Session
|
||||||
|
{
|
||||||
|
public static User CurrentUser { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Клиент, для которого формируется заказ: сам пользователь или клиент, выбранный менеджером.
|
||||||
|
/// </summary>
|
||||||
|
public static User OrderClient { get; private set; }
|
||||||
|
public static List<OrderItem> Cart { get; } = new List<OrderItem>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Сколько пар данной товарной позиции уже лежит в формируемом заказе.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
43
ChudoObuv/MainWindow.xaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<Window x:Class="ChudoObuv.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Чудо Обувь"
|
||||||
|
Icon="Resources/app.ico"
|
||||||
|
Width="1100" Height="750" MinWidth="900" MinHeight="600"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
FontFamily="Calibri" FontSize="16"
|
||||||
|
Background="{StaticResource MainBackgroundBrush}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Шапка: логотип, заголовок страницы, ФИО пользователя -->
|
||||||
|
<Grid Grid.Row="0" Background="{StaticResource SecondaryBackgroundBrush}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image Grid.Column="0" Source="Resources/logo.png" Height="64" Margin="10,6"
|
||||||
|
Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
|
<Button Grid.Column="1" x:Name="BackButton" Content="← Назад" VerticalAlignment="Center"
|
||||||
|
Click="BackButton_Click" />
|
||||||
|
<TextBlock Grid.Column="2" x:Name="PageTitleText" Text="Чудо Обувь" FontSize="26"
|
||||||
|
FontWeight="Bold"
|
||||||
|
VerticalAlignment="Center" Margin="16,0" TextTrimming="CharacterEllipsis" />
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Vertical" VerticalAlignment="Top"
|
||||||
|
HorizontalAlignment="Right" Margin="10,6">
|
||||||
|
<TextBlock x:Name="UserNameText" FontSize="18" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Right" />
|
||||||
|
<TextBlock x:Name="UserRoleText" FontSize="14" HorizontalAlignment="Right" />
|
||||||
|
<Button x:Name="LogoutButton" Content="Выйти" HorizontalAlignment="Right"
|
||||||
|
Click="LogoutButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Frame Grid.Row="1" x:Name="MainFrame" NavigationUIVisibility="Hidden"
|
||||||
|
Navigated="MainFrame_Navigated" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
79
ChudoObuv/MainWindow.xaml.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Pages;
|
||||||
|
|
||||||
|
namespace ChudoObuv
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
MainFrame.Navigate(new LoginPage());
|
||||||
|
}
|
||||||
|
private void MainFrame_Navigated(object sender, NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
// Заголовок окна и шапки берется из заголовка открытой страницы
|
||||||
|
if (e.Content is Page page)
|
||||||
|
{
|
||||||
|
Title = $"Чудо Обувь — {page.Title}";
|
||||||
|
PageTitleText.Text = page.Title;
|
||||||
|
}
|
||||||
|
BackButton.Visibility = MainFrame.CanGoBack ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
UpdateUserInfo();
|
||||||
|
}
|
||||||
|
private void UpdateUserInfo()
|
||||||
|
{
|
||||||
|
bool isLoginPage = MainFrame.Content is LoginPage;
|
||||||
|
if (Session.IsAuthorized)
|
||||||
|
{
|
||||||
|
UserNameText.Text = Session.CurrentUser.FullName;
|
||||||
|
UserRoleText.Text = Session.CurrentUser.Role;
|
||||||
|
|
||||||
|
LogoutButton.Content = "Выйти";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserNameText.Text = isLoginPage ? "" : "Гость";
|
||||||
|
UserRoleText.Text = "";
|
||||||
|
LogoutButton.Content = "Войти";
|
||||||
|
}
|
||||||
|
LogoutButton.Visibility = isLoginPage ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
private void BackButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (MainFrame.CanGoBack)
|
||||||
|
{
|
||||||
|
MainFrame.GoBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void LogoutButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Session.Cart.Count > 0 && !Messages.Confirm("Неподтвержденный заказ будет отменен. Выйти из системы?"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.Logout();
|
||||||
|
MainFrame.Navigate(new LoginPage());
|
||||||
|
ClearHistory();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Очищает историю переходов, чтобы кнопкой «Назад» нельзя было вернуться в чужой сеанс.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearHistory()
|
||||||
|
{
|
||||||
|
// Навигация выполняется асинхронно, поэтому очищаем журнал после завершения перехода
|
||||||
|
Dispatcher.BeginInvoke(new Action(() =>
|
||||||
|
{
|
||||||
|
while (MainFrame.CanGoBack)
|
||||||
|
{
|
||||||
|
MainFrame.RemoveBackEntry();
|
||||||
|
}
|
||||||
|
BackButton.Visibility = Visibility.Collapsed;
|
||||||
|
UpdateUserInfo();
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.ContextIdle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
ChudoObuv/Models/Order.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Models
|
||||||
|
{
|
||||||
|
public class Order
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime OrderDate { get; set; }
|
||||||
|
public int ClientId { get; set; }
|
||||||
|
public string ClientFullName { get; set; }
|
||||||
|
public int ItemsCount { get; set; }
|
||||||
|
public decimal TotalSum { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
25
ChudoObuv/Models/OrderItem.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Строка состава заказа (сохраненная в БД или в оформляемом заказе).
|
||||||
|
/// </summary>
|
||||||
|
public class OrderItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int StockItemId { get; set; }
|
||||||
|
public string ProductName { get; set; }
|
||||||
|
public string Manufacturer { get; set; }
|
||||||
|
public decimal Size { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public decimal UnitPrice { get; set; }
|
||||||
|
public string Position => $"{ProductName}, {Manufacturer}, размер {Size:0.#}";
|
||||||
|
public string SizeText => Size.ToString("0.#");
|
||||||
|
public decimal Sum => UnitPrice * Quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
ChudoObuv/Models/Product.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.IO;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Товар (модель обуви) для вывода в каталоге.
|
||||||
|
/// </summary>
|
||||||
|
public class Product
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public string Subcategory { get; set; }
|
||||||
|
public string Manufacturer { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Composition { get; set; }
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
public string ImageFile { get; set; }
|
||||||
|
public int TotalQuantity { get; set; }
|
||||||
|
public bool HasOrdersInPreviousMonth { get; set; }
|
||||||
|
public string Title => $"{Manufacturer} | {Name}";
|
||||||
|
public decimal DiscountedPrice => PriceCalculator.CalculateDiscountedPrice(Price,
|
||||||
|
HasOrdersInPreviousMonth);
|
||||||
|
public bool HasDiscount => DiscountedPrice < Price;
|
||||||
|
public string QuantityLabel => PriceCalculator.GetQuantityLabel(TotalQuantity);
|
||||||
|
public bool IsLowStock => PriceCalculator.IsLowStock(TotalQuantity);
|
||||||
|
/// <summary>
|
||||||
|
/// Путь к изображению товара; если файла нет — картинка-заглушка.
|
||||||
|
/// </summary>
|
||||||
|
public string ImagePath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string folder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources");
|
||||||
|
if (!string.IsNullOrWhiteSpace(ImageFile))
|
||||||
|
{
|
||||||
|
string path = Path.Combine(folder, "Images", ImageFile);
|
||||||
|
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Path.Combine(folder, "picture.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ChudoObuv/Models/StockItem.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Товарная позиция: модель + размер с доступным количеством.
|
||||||
|
/// </summary>
|
||||||
|
public class StockItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ProductId { get; set; }
|
||||||
|
public decimal Size { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public string SizeText => Size.ToString("0.#");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
ChudoObuv/Models/User.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Models
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public const string AdminRole = "Администратор";
|
||||||
|
public const string ManagerRole = "Менеджер";
|
||||||
|
public const string ClientRole = "Авторизованный пользователь";
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string LastName { get; set; }
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
public string Patronymic { get; set; }
|
||||||
|
public string Login { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public string FullName => $"{LastName} {FirstName} {Patronymic}".Trim();
|
||||||
|
public bool IsAdmin => Role == AdminRole;
|
||||||
|
public bool IsManagerOrAdmin => Role == ManagerRole || Role == AdminRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
ChudoObuv/Pages/CartPage.xaml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.CartPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Оформление заказа"
|
||||||
|
Loaded="Page_Loaded">
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" x:Name="ClientText" FontSize="18" Margin="4" TextWrapping="Wrap" />
|
||||||
|
<DataGrid Grid.Row="1" x:Name="ItemsDataGrid">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Товар" Binding="{Binding ProductName}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Производство" Binding="{Binding Manufacturer}" Width="170"
|
||||||
|
/>
|
||||||
|
<DataGridTextColumn Header="Размер" Binding="{Binding SizeText}" Width="80" />
|
||||||
|
<DataGridTextColumn Header="Количество" Binding="{Binding Quantity}" Width="100" />
|
||||||
|
<DataGridTextColumn Header="Цена за ед., ₽" Binding="{Binding UnitPrice,
|
||||||
|
StringFormat=N2}" Width="120" />
|
||||||
|
<DataGridTextColumn Header="Сумма, ₽" Binding="{Binding Sum, StringFormat=N2}"
|
||||||
|
Width="120" />
|
||||||
|
<DataGridTemplateColumn Header="" Width="110">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Content="Удалить" Margin="2" Padding="6,0" MinHeight="26"
|
||||||
|
Click="RemoveItemButton_Click" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<TextBlock Grid.Row="2" x:Name="TotalText" FontSize="22" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Right" Margin="4,8" />
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Продолжить выбор товаров" Click="ContinueButton_Click" />
|
||||||
|
<Button Content="Отказаться от заказа" Click="CancelOrderButton_Click" />
|
||||||
|
<Button Content="Подтвердить заказ" Style="{StaticResource AccentButton}"
|
||||||
|
Click="ConfirmButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
102
ChudoObuv/Pages/CartPage.xaml.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using ChudoObuv.Pages;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class CartPage : Page
|
||||||
|
{
|
||||||
|
public CartPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshCart();
|
||||||
|
}
|
||||||
|
private void RefreshCart()
|
||||||
|
{
|
||||||
|
ClientText.Text = $"Клиент: {Session.OrderClient.FullName} Дата заказа: { DateTime.Today:dd.MM.yyyy}";
|
||||||
|
ItemsDataGrid.ItemsSource = null;
|
||||||
|
ItemsDataGrid.ItemsSource = Session.Cart;
|
||||||
|
TotalText.Text = $"Итого: {Session.Cart.Sum(item => item.Sum):N2} ₽";
|
||||||
|
}
|
||||||
|
private void RemoveItemButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (((Button)sender).DataContext is OrderItem item &&
|
||||||
|
Messages.Confirm($"Удалить из заказа «{item.ProductName}», размер {item.SizeText}?"))
|
||||||
|
{
|
||||||
|
Session.Cart.Remove(item);
|
||||||
|
RefreshCart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void ContinueButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CatalogPage());
|
||||||
|
}
|
||||||
|
private void CancelOrderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!Messages.Confirm("Отказаться от заказа? Все выбранные товары будут удалены из заказа. Действие нельзя отменить."))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.CancelOrder();
|
||||||
|
GoHome();
|
||||||
|
}
|
||||||
|
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Session.Cart.Count == 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
Messages.ShowWarning("В заказе нет товаров. Выберите товар в каталоге, укажите размер и количество и нажмите «Добавить в заказ».");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Messages.Confirm($"Подтвердить заказ на сумму {Session.Cart.Sum(item => item.Sum):N2} ₽для клиента { Session.OrderClient.FullName}?"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int orderId = DataService.CreateOrder(Session.OrderClient.Id, Session.Cart);
|
||||||
|
Messages.ShowInfo($"Заказ No {orderId} оформлен {DateTime.Today:dd.MM.yyyy}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.CancelOrder();
|
||||||
|
GoHome();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Менеджер и администратор возвращаются к списку заказов, клиент — в каталог.
|
||||||
|
/// </summary>
|
||||||
|
private void GoHome()
|
||||||
|
{
|
||||||
|
if (Session.CurrentUser.IsManagerOrAdmin)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new OrdersPage());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CatalogPage());
|
||||||
|
}
|
||||||
|
((MainWindow)Application.Current.MainWindow).ClearHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
ChudoObuv/Pages/CatalogPage.xaml
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.CatalogPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Каталог товаров"
|
||||||
|
Loaded="Page_Loaded">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Поиск, фильтрация и сортировка: только для авторизованных пользователей -->
|
||||||
|
<Grid Grid.Row="0" x:Name="FilterPanel">
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="230" />
|
||||||
|
<ColumnDefinition Width="230" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Поиск по наименованию и описанию:" Margin="4,0" />
|
||||||
|
<TextBox x:Name="SearchTextBox" TextChanged="Filter_Changed" MaxLength="100" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Text="Категория:" Margin="4,0" />
|
||||||
|
<ComboBox x:Name="CategoryComboBox" SelectionChanged="Filter_Changed" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock Text="Сортировка по цене:" Margin="4,0" />
|
||||||
|
<ComboBox x:Name="SortComboBox" SelectionChanged="Filter_Changed">
|
||||||
|
<ComboBoxItem Content="Без сортировки" IsSelected="True" />
|
||||||
|
<ComboBoxItem Content="По возрастанию цены" />
|
||||||
|
<ComboBoxItem Content="По убыванию цены" />
|
||||||
|
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<!-- Действия с заказом -->
|
||||||
|
<DockPanel Grid.Row="1" Margin="0,4">
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||||
|
<Button x:Name="CartButton" Style="{StaticResource AccentButton}"
|
||||||
|
Click="CartButton_Click" />
|
||||||
|
<Button x:Name="OrdersButton" Content="Список заказов" Click="OrdersButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="OrderClientText" VerticalAlignment="Center" Margin="4,0"
|
||||||
|
FontStyle="Italic" TextWrapping="Wrap" />
|
||||||
|
</DockPanel>
|
||||||
|
<ListView Grid.Row="2" x:Name="ProductsListView"
|
||||||
|
SelectionChanged="ProductsListView_SelectionChanged"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
BorderThickness="0">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border BorderBrush="Black" BorderThickness="1" Padding="8" Margin="2">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource
|
||||||
|
MainBackgroundBrush}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<!-- Остаток по всем размерам три пары и меньше — подсветка -->
|
||||||
|
<DataTrigger Binding="{Binding IsLowStock}" Value="True">
|
||||||
|
<Setter Property="Background" Value="{StaticResource
|
||||||
|
LowStockBrush}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="170" />
|
||||||
|
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image Grid.Column="0" Source="{Binding ImagePath}" Height="110"
|
||||||
|
Stretch="Uniform" Margin="0,0,12,0" />
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Title}" FontSize="20" FontWeight="Bold"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock Margin="0,4,0,0" FontSize="17">
|
||||||
|
<Run Text="Категория:" /> <Run Text="{Binding Category,
|
||||||
|
Mode=OneWay}" />
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock FontSize="17">
|
||||||
|
|
||||||
|
<Run Text="Количество:" /> <Run Text="{Binding QuantityLabel,
|
||||||
|
Mode=OneWay}" FontWeight="Bold" />
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
|
||||||
|
<TextBlock FontSize="15" TextWrapping="Wrap">
|
||||||
|
<Run Text="Состав:" /> <Run Text="{Binding Composition,
|
||||||
|
Mode=OneWay}" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<TextBlock Text="{Binding Price, StringFormat={}{0:N2} ₽}" FontSize="16"
|
||||||
|
TextDecorations="Strikethrough" Foreground="DimGray"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
|
||||||
|
<Style.Triggers>
|
||||||
|
|
||||||
|
<DataTrigger Binding="{Binding HasDiscount}"
|
||||||
|
Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding DiscountedPrice, StringFormat={}{0:N2} ₽}"
|
||||||
|
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="Bold" HorizontalAlignment="Right" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
<TextBlock Grid.Row="3" x:Name="CountText" Margin="4" Foreground="DimGray" />
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
131
ChudoObuv/Pages/CatalogPage.xaml.cs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class CatalogPage : Page
|
||||||
|
{
|
||||||
|
private const string AllCategories = "Все категории";
|
||||||
|
private List<Product> allProducts = new List<Product>();
|
||||||
|
private bool isInitialized;
|
||||||
|
public CatalogPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
// Вызывается при каждом открытии страницы, в том числе после возврата «Назад»,
|
||||||
|
// поэтому каталог и остатки всегда актуальны
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
bool isAuthorized = Session.IsAuthorized;
|
||||||
|
FilterPanel.Visibility = isAuthorized ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
CartButton.Visibility = isAuthorized ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
OrdersButton.Visibility = isAuthorized && Session.CurrentUser.IsManagerOrAdmin ?
|
||||||
|
Visibility.Visible : Visibility.Collapsed;
|
||||||
|
CartButton.Content = $"Оформление заказа ({Session.Cart.Sum(item => item.Quantity)})";
|
||||||
|
OrderClientText.Text = isAuthorized ? $"Заказ оформляется для клиента: {Session.OrderClient.FullName}" : "Для оформления заказа войдите в систему.";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!isInitialized)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
List<string> categories = DataService.GetCategories();
|
||||||
|
|
||||||
|
categories.Insert(0, AllCategories);
|
||||||
|
CategoryComboBox.ItemsSource = categories;
|
||||||
|
CategoryComboBox.SelectedIndex = 0;
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
allProducts = DataService.GetProducts();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
allProducts = new List<Product>();
|
||||||
|
}
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
private void Filter_Changed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Поиск, фильтр и сортировка применяются вместе при любом изменении.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyFilters()
|
||||||
|
{
|
||||||
|
if (ProductsListView == null || SortComboBox == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IEnumerable<Product> result = allProducts;
|
||||||
|
if (Session.IsAuthorized)
|
||||||
|
{
|
||||||
|
string search = SearchTextBox.Text.Trim();
|
||||||
|
if (search.Length > 0)
|
||||||
|
{
|
||||||
|
result = result.Where(product =>
|
||||||
|
product.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
product.Description.Contains(search, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
}
|
||||||
|
string category = CategoryComboBox.SelectedItem as string;
|
||||||
|
if (category != null && category != AllCategories)
|
||||||
|
{
|
||||||
|
result = result.Where(product => product.Category == category);
|
||||||
|
}
|
||||||
|
if (SortComboBox.SelectedIndex == 1)
|
||||||
|
{
|
||||||
|
result = result.OrderBy(product => product.DiscountedPrice);
|
||||||
|
}
|
||||||
|
else if (SortComboBox.SelectedIndex == 2)
|
||||||
|
{
|
||||||
|
result = result.OrderByDescending(product => product.DiscountedPrice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Product> shown = result.ToList();
|
||||||
|
ProductsListView.ItemsSource = shown;
|
||||||
|
CountText.Text = $"Показано товаров: {shown.Count} из {allProducts.Count}";
|
||||||
|
}
|
||||||
|
private void ProductsListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ProductsListView.SelectedItem is not Product product)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProductsListView.SelectedItem = null;
|
||||||
|
if (!Session.IsAuthorized)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
Messages.ShowInfo("Оформлять заказы могут только авторизованные пользователи.\nВыйдите на страницу входа и войдите под своим логином.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NavigationService.Navigate(new ProductPage(product));
|
||||||
|
}
|
||||||
|
private void CartButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CartPage());
|
||||||
|
}
|
||||||
|
private void OrdersButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new OrdersPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
ChudoObuv/Pages/LoginPage.xaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.LoginPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Вход в систему">
|
||||||
|
<Grid>
|
||||||
|
<Border Width="420" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="24"
|
||||||
|
Background="{StaticResource SecondaryBackgroundBrush}" BorderBrush="{StaticResource
|
||||||
|
AccentBrush}"
|
||||||
|
BorderThickness="2" CornerRadius="8">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Вход в систему" FontSize="24" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||||||
|
<TextBlock Text="Логин:" Margin="4,0" />
|
||||||
|
<TextBox x:Name="LoginTextBox" MaxLength="50" KeyDown="LoginTextBox_KeyDown"
|
||||||
|
ToolTip="Введите логин из списка пользователей системы. Пароль не требуется."
|
||||||
|
/>
|
||||||
|
<TextBlock Text="Пароль вводить не требуется." FontSize="13" Foreground="DimGray"
|
||||||
|
Margin="4,0,4,12" />
|
||||||
|
<Button Content="Войти" Style="{StaticResource AccentButton}" Click="LoginButton_Click"
|
||||||
|
/>
|
||||||
|
<Button Content="Войти как гость" Click="GuestButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
79
ChudoObuv/Pages/LoginPage.xaml.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class LoginPage : Page
|
||||||
|
{
|
||||||
|
public LoginPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += (sender, e) => LoginTextBox.Focus();
|
||||||
|
}
|
||||||
|
private void LoginButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
TryLogin();
|
||||||
|
}
|
||||||
|
private void LoginTextBox_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
TryLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void TryLogin()
|
||||||
|
{
|
||||||
|
string login = LoginTextBox.Text.Trim();
|
||||||
|
if (login.Length == 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
Messages.ShowWarning("Введите логин. Логин выдается администратором и указан в списке пользователей системы.");
|
||||||
|
LoginTextBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User user;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user = DataService.FindUserByLogin(login);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
Messages.ShowError($"Пользователь с логином «{login}» не найден.\nПроверьте раскладку клавиатуры и правильность ввода логина или войдите как гость.");
|
||||||
|
LoginTextBox.SelectAll();
|
||||||
|
LoginTextBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.Login(user);
|
||||||
|
OpenCatalog();
|
||||||
|
}
|
||||||
|
private void GuestButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Session.Logout();
|
||||||
|
OpenCatalog();
|
||||||
|
}
|
||||||
|
private void OpenCatalog()
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CatalogPage());
|
||||||
|
((MainWindow)Application.Current.MainWindow).ClearHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
ChudoObuv/Pages/OrderPage.xaml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.OrderPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Состав заказа"
|
||||||
|
Loaded="Page_Loaded">
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" x:Name="HeaderText" FontSize="20" FontWeight="Bold" Margin="4"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="Дата заказа:" VerticalAlignment="Center" Margin="4,0" />
|
||||||
|
<TextBlock x:Name="OrderDateText" VerticalAlignment="Center" FontWeight="Bold" />
|
||||||
|
<!-- Редактирование даты доступно только администратору -->
|
||||||
|
<DatePicker x:Name="OrderDatePicker" Width="160" Margin="4" FontSize="16" />
|
||||||
|
<Button x:Name="SaveDateButton" Content="Сохранить дату" Style="{StaticResource
|
||||||
|
AccentButton}" Click="SaveDateButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
<DataGrid Grid.Row="2" x:Name="ItemsDataGrid">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Товарная позиция (модель, производство, размер)"
|
||||||
|
Binding="{Binding Position}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Количество" Binding="{Binding Quantity}" Width="110" />
|
||||||
|
<DataGridTextColumn Header="Цена за ед., ₽" Binding="{Binding UnitPrice,
|
||||||
|
StringFormat=N2}" Width="130" />
|
||||||
|
<DataGridTextColumn Header="Сумма, ₽" Binding="{Binding Sum, StringFormat=N2}"
|
||||||
|
Width="130" />
|
||||||
|
<DataGridTemplateColumn x:Name="DeleteColumn" Header="" Width="170">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Content="Удалить позицию" Margin="2" Padding="6,0" MinHeight="26"
|
||||||
|
Click="DeleteItemButton_Click" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<TextBlock Grid.Row="3" x:Name="TotalText" FontSize="22" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Right" Margin="4,8" />
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
109
ChudoObuv/Pages/OrderPage.xaml.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Globalization;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class OrderPage : Page
|
||||||
|
{
|
||||||
|
private readonly Order order;
|
||||||
|
private List<OrderItem> items = new List<OrderItem>();
|
||||||
|
public OrderPage(Order order)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.order = order;
|
||||||
|
Title = $"Состав заказа No {order.Id}";
|
||||||
|
}
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
bool isAdmin = Session.CurrentUser.IsAdmin;
|
||||||
|
OrderDateText.Visibility = isAdmin ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
OrderDatePicker.Visibility = isAdmin ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
SaveDateButton.Visibility = isAdmin ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
DeleteColumn.Visibility = isAdmin ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
HeaderText.Text = $"Заказ No {order.Id}. Клиент: {order.ClientFullName}";
|
||||||
|
OrderDateText.Text = order.OrderDate.ToString("dd.MM.yyyy");
|
||||||
|
OrderDatePicker.SelectedDate = order.OrderDate;
|
||||||
|
LoadItems();
|
||||||
|
}
|
||||||
|
private void LoadItems()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
items = DataService.GetOrderItems(order.Id);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
items = new List<OrderItem>();
|
||||||
|
}
|
||||||
|
ItemsDataGrid.ItemsSource = items;
|
||||||
|
TotalText.Text = $"Итоговая сумма по заказу: {items.Sum(item => item.Sum):N2} ₽";
|
||||||
|
}
|
||||||
|
private void SaveDateButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Дату, введенную с клавиатуры, разбираем сами: DatePicker применяет ее только при потере фокуса
|
||||||
|
if (!DateTime.TryParseExact(OrderDatePicker.Text.Trim(), "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime newDate))
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Дата заказа указана неверно. Выберите ее в календаре или введите в формате ДД.ММ.ГГГГ, например 15.04.2026.");
|
||||||
|
OrderDatePicker.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newDate.Date > DateTime.Today)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Дата заказа не может быть позже сегодняшней. Выберите другую дату.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DataService.UpdateOrderDate(order.Id, newDate);
|
||||||
|
order.OrderDate = newDate;
|
||||||
|
Messages.ShowInfo($"Дата заказа No {order.Id} изменена на {newDate:dd.MM.yyyy}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void DeleteItemButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (((Button)sender).DataContext is not OrderItem item)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (items.Count == 1)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Это последняя позиция заказа. Пустой заказ хранить нельзя — удалите весь заказ на странице «Список заказов».");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Messages.Confirm($"Удалить позицию «{item.Position}» из заказа No {order.Id}?\nТовар вернется на склад.Действие нельзя отменить."))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DataService.DeleteOrderItem(item.Id);
|
||||||
|
LoadItems();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
ChudoObuv/Pages/OrdersPage.xaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.OrdersPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Список заказов"
|
||||||
|
Loaded="Page_Loaded">
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Новый заказ: сначала выбирается клиент -->
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource SecondaryBackgroundBrush}" Padding="8"
|
||||||
|
CornerRadius="6" Margin="0,0,0,8">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="Новый заказ для клиента:" VerticalAlignment="Center" Margin="4,0" />
|
||||||
|
<ComboBox x:Name="ClientComboBox" Width="320" DisplayMemberPath="FullName" />
|
||||||
|
<Button Content="Добавить заказ" Style="{StaticResource AccentButton}"
|
||||||
|
Click="AddOrderButton_Click" />
|
||||||
|
<Button Content="Каталог товаров" Click="CatalogButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<DataGrid Grid.Row="1" x:Name="OrdersDataGrid"
|
||||||
|
MouseDoubleClick="OrdersDataGrid_MouseDoubleClick">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="No заказа" Binding="{Binding Id}" Width="90" />
|
||||||
|
<DataGridTextColumn Header="Дата заказа" Binding="{Binding OrderDate,
|
||||||
|
StringFormat=dd.MM.yyyy}" Width="130" />
|
||||||
|
<DataGridTextColumn Header="ФИО клиента" Binding="{Binding ClientFullName}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Позиций" Binding="{Binding ItemsCount}" Width="90" />
|
||||||
|
<DataGridTextColumn Header="Сумма, ₽" Binding="{Binding TotalSum, StringFormat=N2}"
|
||||||
|
Width="130" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||||
|
<TextBlock Text="Двойной щелчок по заказу — просмотр состава" VerticalAlignment="Center"
|
||||||
|
Foreground="DimGray" Margin="8,0" />
|
||||||
|
<Button Content="Состав заказа" Click="OpenOrderButton_Click" />
|
||||||
|
<Button Content="Удалить заказ" Click="DeleteOrderButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
111
ChudoObuv/Pages/OrdersPage.xaml.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class OrdersPage : Page
|
||||||
|
{
|
||||||
|
public OrdersPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// Список перечитывается при каждом открытии страницы (после добавления, изменения, удаления)
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ClientComboBox.ItemsSource == null)
|
||||||
|
{
|
||||||
|
ClientComboBox.ItemsSource = DataService.GetClients();
|
||||||
|
}
|
||||||
|
OrdersDataGrid.ItemsSource = DataService.GetOrders();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Order GetSelectedOrder()
|
||||||
|
{
|
||||||
|
if (OrdersDataGrid.SelectedItem is Order order)
|
||||||
|
{
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
Messages.ShowWarning("Сначала выберите заказ в списке (щелкните по строке).");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private void AddOrderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ClientComboBox.SelectedItem is not User client)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Выберите клиента, для которого оформляется заказ.");
|
||||||
|
ClientComboBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Session.Cart.Count > 0 && !Messages.Confirm("Ранее начатый неподтвержденный заказ будет отменен.Продолжить ? "))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.StartOrderFor(client);
|
||||||
|
NavigationService.Navigate(new CatalogPage());
|
||||||
|
}
|
||||||
|
private void CatalogButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CatalogPage());
|
||||||
|
}
|
||||||
|
private void OpenOrderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Order order = GetSelectedOrder();
|
||||||
|
if (order != null)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new OrderPage(order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void OrdersDataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (OrdersDataGrid.SelectedItem is Order order)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new OrderPage(order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void DeleteOrderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Order order = GetSelectedOrder();
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Messages.Confirm($"Удалить заказ No {order.Id} от {order.OrderDate:dd.MM.yyyy} ({ order.ClientFullName})?\n" + "Товары вернутся на склад. Действие нельзя отменить."))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DataService.DeleteOrder(order.Id);
|
||||||
|
OrdersDataGrid.ItemsSource = DataService.GetOrders();
|
||||||
|
Messages.ShowInfo($"Заказ No {order.Id} удален.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
ChudoObuv/Pages/ProductPage.xaml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<Page x:Class="ChudoObuv.Pages.ProductPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Calibri" FontSize="16" Background="{StaticResource MainBackgroundBrush}"
|
||||||
|
Title="Просмотр товара"
|
||||||
|
Loaded="Page_Loaded">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="340" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0" BorderBrush="{StaticResource AccentBrush}" BorderThickness="1"
|
||||||
|
Padding="8" VerticalAlignment="Top">
|
||||||
|
<Image x:Name="ProductImage" Height="280" Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Column="1" Margin="20,0,0,0">
|
||||||
|
<TextBlock x:Name="NameText" FontSize="26" FontWeight="Bold" TextWrapping="Wrap" />
|
||||||
|
<TextBlock Margin="0,6,0,0"><Run Text="Производство:" FontWeight="Bold" /> <Run
|
||||||
|
x:Name="ManufacturerRun" /></TextBlock>
|
||||||
|
<TextBlock><Run Text="Категория:" FontWeight="Bold" /> <Run x:Name="CategoryRun"
|
||||||
|
/></TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,6">
|
||||||
|
<TextBlock Text="Цена со скидкой:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="DiscountedPriceText" FontSize="24" FontWeight="Bold" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="OldPriceText" TextDecorations="Strikethrough"
|
||||||
|
Foreground="DimGray" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock TextWrapping="Wrap"><Run Text="Состав:" FontWeight="Bold" /> <Run
|
||||||
|
x:Name="CompositionRun" /></TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0,6,0,0"><Run Text="Описание:" FontWeight="Bold"
|
||||||
|
/> <Run x:Name="DescriptionRun" /></TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0,6,0,0"><Run Text="Размерный ряд:"
|
||||||
|
FontWeight="Bold" /> <Run x:Name="SizeRangeRun" /></TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap"><Run Text="Доступные для заказа размеры:"
|
||||||
|
FontWeight="Bold" /> <Run x:Name="AvailableSizesRun" /></TextBlock>
|
||||||
|
<Border Background="{StaticResource SecondaryBackgroundBrush}" Padding="12"
|
||||||
|
Margin="0,16,0,0" CornerRadius="6">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Добавление в заказ" FontSize="20" FontWeight="Bold" />
|
||||||
|
<Grid Margin="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="160" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="100" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="0" Text="Размер:" VerticalAlignment="Center" />
|
||||||
|
<ComboBox Grid.Column="1" x:Name="SizeComboBox" DisplayMemberPath="SizeText"
|
||||||
|
|
||||||
|
SelectionChanged="SizeComboBox_SelectionChanged" />
|
||||||
|
<TextBlock Grid.Column="2" Text="Количество:" VerticalAlignment="Center"
|
||||||
|
Margin="12,0,0,0" />
|
||||||
|
<TextBox Grid.Column="3" x:Name="QuantityTextBox" Text="1" MaxLength="3"
|
||||||
|
PreviewTextInput="QuantityTextBox_PreviewTextInput"
|
||||||
|
|
||||||
|
ToolTip="Введите целое число пар от 1 до доступного количества" />
|
||||||
|
<TextBlock Grid.Column="4" x:Name="AvailableText" VerticalAlignment="Center"
|
||||||
|
Margin="8,0" Foreground="DimGray" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||||
|
|
||||||
|
<Button Content="Добавить в заказ" Style="{StaticResource AccentButton}"
|
||||||
|
Click="AddButton_Click" />
|
||||||
|
<Button Content="Перейти к оформлению заказа" Click="CartButton_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Page>
|
||||||
126
ChudoObuv/Pages/ProductPage.xaml.cs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using ChudoObuv.Data;
|
||||||
|
using ChudoObuv.Logic;
|
||||||
|
using ChudoObuv.Models;
|
||||||
|
namespace ChudoObuv.Pages
|
||||||
|
{
|
||||||
|
public partial class ProductPage : Page
|
||||||
|
{
|
||||||
|
private readonly Product product;
|
||||||
|
private List<StockItem> stockItems = new List<StockItem>();
|
||||||
|
public ProductPage(Product product)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.product = product;
|
||||||
|
Title = $"Просмотр товара: {product.Name}";
|
||||||
|
}
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ProductImage.Source = new BitmapImage(new Uri(product.ImagePath));
|
||||||
|
NameText.Text = product.Name;
|
||||||
|
ManufacturerRun.Text = product.Manufacturer;
|
||||||
|
CategoryRun.Text = $"{product.Category} / {product.Subcategory}";
|
||||||
|
DiscountedPriceText.Text = $"{product.DiscountedPrice:N2} ₽";
|
||||||
|
OldPriceText.Text = product.HasDiscount ? $"{product.Price:N2} ₽" : "";
|
||||||
|
CompositionRun.Text = product.Composition;
|
||||||
|
DescriptionRun.Text = product.Description;
|
||||||
|
LoadSizes();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает актуальные остатки из БД и учитывает то, что уже добавлено в заказ.
|
||||||
|
/// </summary>
|
||||||
|
private void LoadSizes()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stockItems = DataService.GetStockItems(product.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Messages.ShowDatabaseError(ex);
|
||||||
|
stockItems = new List<StockItem>();
|
||||||
|
}
|
||||||
|
SizeRangeRun.Text = stockItems.Count > 0 ? string.Join(", ", stockItems.Select(item =>
|
||||||
|
item.SizeText)) : "нет данных";
|
||||||
|
List<StockItem> available = stockItems
|
||||||
|
.Where(item => item.Quantity - Session.GetQuantityInCart(item.Id) > 0)
|
||||||
|
.ToList();
|
||||||
|
AvailableSizesRun.Text = available.Count > 0 ? string.Join(", ", available.Select(item =>
|
||||||
|
item.SizeText)) : "нет в наличии";
|
||||||
|
SizeComboBox.ItemsSource = available;
|
||||||
|
SizeComboBox.SelectedIndex = available.Count > 0 ? 0 : -1;
|
||||||
|
UpdateAvailableText();
|
||||||
|
}
|
||||||
|
private int GetAvailableQuantity(StockItem stockItem)
|
||||||
|
{
|
||||||
|
return stockItem.Quantity - Session.GetQuantityInCart(stockItem.Id);
|
||||||
|
}
|
||||||
|
private void SizeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
|
||||||
|
{
|
||||||
|
UpdateAvailableText();
|
||||||
|
}
|
||||||
|
private void UpdateAvailableText()
|
||||||
|
{
|
||||||
|
AvailableText.Text = SizeComboBox.SelectedItem is StockItem stockItem
|
||||||
|
? $"доступно: {GetAvailableQuantity(stockItem)} пар"
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
// В поле количества можно вводить только цифры
|
||||||
|
private void QuantityTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = !e.Text.All(char.IsDigit);
|
||||||
|
}
|
||||||
|
private void AddButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (SizeComboBox.SelectedItem is not StockItem stockItem)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Выберите размер обуви. Если список размеров пуст — товара нет в наличии.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int available = GetAvailableQuantity(stockItem);
|
||||||
|
if (!int.TryParse(QuantityTextBox.Text, out int quantity) || quantity <= 0)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning("Количество указано неверно. Введите целое число пар больше нуля.");
|
||||||
|
QuantityTextBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (quantity > available)
|
||||||
|
{
|
||||||
|
Messages.ShowWarning($"Для размера {stockItem.SizeText} доступно только {available} пар. Уменьшите количество.");
|
||||||
|
QuantityTextBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.AddToCart(new OrderItem
|
||||||
|
{
|
||||||
|
StockItemId = stockItem.Id,
|
||||||
|
ProductName = product.Name,
|
||||||
|
Manufacturer = product.Manufacturer,
|
||||||
|
Size = stockItem.Size,
|
||||||
|
Quantity = quantity,
|
||||||
|
UnitPrice = product.DiscountedPrice
|
||||||
|
});
|
||||||
|
Messages.ShowInfo($"Товар «{product.Name}», размер {stockItem.SizeText}, {quantity} пар добавлен в заказ.\n" +
|
||||||
|
"Добавьте другие товары или перейдите к оформлению заказа.");
|
||||||
|
QuantityTextBox.Text = "1";
|
||||||
|
LoadSizes();
|
||||||
|
}
|
||||||
|
private void CartButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(new CartPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ChudoObuv/Resources/Images/IMG_KB_164811.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KB_164816.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KB_185501.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KS_195213.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KS_195217.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KS_195222.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KSd_185508.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KSd_195207.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
ChudoObuv/Resources/Images/IMG_KSho_185512.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MB_174349.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MB_191200.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MB_191205.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MS_185531.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MS_185532.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MS_185533.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MSho_190514.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MSho_190518.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
ChudoObuv/Resources/Images/IMG_MSho_191612.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WB_170859.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WB_170905.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WB_170906.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WS_174110.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WS_185528.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WS_185535.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WS_185538.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSd_174332.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSd_185156.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSd_185551.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSho_174409.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSho_185202.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
ChudoObuv/Resources/Images/IMG_WSho_185545.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
ChudoObuv/Resources/app.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ChudoObuv/Resources/logo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
ChudoObuv/Resources/picture.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v8.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v8.0": {
|
||||||
|
"ChudoObuv/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"MySqlConnector": "2.6.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"ChudoObuv.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MySqlConnector/2.6.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MySqlConnector.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"ChudoObuv/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
|
||||||
|
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||||
|
"path": "microsoft.extensions.logging.abstractions/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MySqlConnector/2.6.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-dlTc/tsBa42szdvpj4nrHvQVUb0gsMwR9HT6J0Zz2OSqfP0CdYlGw+qQPqCpbnO97idfvclSjKYtM6y5QaBQaQ==",
|
||||||
|
"path": "mysqlconnector/2.6.2",
|
||||||
|
"hashPath": "mysqlconnector.2.6.2.nupkg.sha512"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ChudoObuv/Исполняемые_файлы/net8.0-windows/ChudoObuv.dll
Normal file
BIN
ChudoObuv/Исполняемые_файлы/net8.0-windows/ChudoObuv.exe
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.WindowsDesktop.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||||
|
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ChudoObuv/Исполняемые_файлы/net8.0-windows/MySqlConnector.dll
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 24 KiB |