# AlfaToad.CustomControls

Библиотека пользовательских контролов для **Windows Forms** (.NET Framework 4.8 и .NET 8).

Пакет: [NuGet — AlfaToad.CustomControls](https://www.nuget.org/packages/AlfaToad.CustomControls)

## Компоненты

| Контрол | Назначение |
|---------|------------|
| `ScrollableDetailPanel` | Прокручиваемый список строк с фото, полями товара и скидкой |
| `ScrollableOrderPanel` | Прокручиваемый список строк заказа |
| `BoundDataGridView` | Таблица с привязкой к `DataSet` и подсветкой столбца |

---

## Установка

```bash
dotnet add package AlfaToad.CustomControls --version 4.2.1
```

```xml
<PackageReference Include="AlfaToad.CustomControls" Version="4.2.1" />
```

---

## ScrollableDetailPanel — настраиваемые свойства

### Поля данных (имена столбцов в `DataTable`)

| Поле на экране | Свойство | Значение по умолчанию |
|----------------|----------|----------------------|
| Фото | `ImageField` | `Фото` |
| Категория | `CategoryField` | `Категория товара` |
| Наименование | `NameField` | `Наименование товара` |
| Описание | `DescriptionField` | `Описание товара` |
| Производитель | `ManufacturerField` | `Производитель` |
| Поставщик | `SupplierField` | `Поставщик` |
| Цена | `PriceField` | `Цена` |
| Единица измерения | `UnitField` | `Единица измерения` |
| Количество на складе | `StockField` | `Кол-во на складе` |
| Скидка | `DiscountField` | `Действующая скидка` |

### Подписи на экране

| Подпись | Свойство | Значение по умолчанию |
|---------|----------|----------------------|
| Описание | `DescriptionCaption` | `Описание товара:` |
| Производитель | `ManufacturerCaption` | `Производитель:` |
| Поставщик | `SupplierCaption` | `Поставщик:` |
| Цена | `PriceCaption` | `Цена:` |
| Единица измерения | `UnitCaption` | `Единица измерения:` |
| Количество на складе | `StockCaption` | `Количество на складе:` |
| Заголовок столбца скидки | `DiscountCaption` | `Действующая скидка` |

### Изображения

| Свойство | Описание |
|----------|----------|
| `NoImage` | Картинка при отсутствии файла или пустом поле. По умолчанию **не задана** — область фото остаётся пустой. |
| `ImagesFolder` | Папка, из которой загружаются файлы по имени из `ImageField`. |

### Размеры строки

| Свойство | По умолчанию |
|----------|--------------|
| `RowHeight` | `160` |
| `ImageColumnWidth` | `120` |
| `DiscountColumnWidth` | `110` |
| `RowSpacing` | `8` (зона сепаратора между строками) |
| `OutOfStockRowColor` | голубой `#ADD8E6` (количество на складе ≤ 0) |
| `RowSeparatorColor` | `Silver` (линия между записями) |
| `ImageLeftPadding` | `8` (отступ фото от левого края) |
| `DataSource` | источник данных в дизайнере (DataSet / DataTable / BindingSource) |
| `DataMember` | имя таблицы в DataSet |

---

## ScrollableOrderPanel — настраиваемые свойства

### Поля данных

| Поле на экране | Свойство | Значение по умолчанию |
|----------------|----------|----------------------|
| Артикул заказа | `ArticleField` | `Артикул заказа` |
| Статус | `StatusField` | `Статус заказа` |
| Адрес пункта выдачи | `AddressField` | `Адрес пункта выдачи` |
| Дата заказа | `OrderDateField` | `Дата заказа` |
| Дата доставки | `DeliveryDateField` | `Дата доставки` |

### Подписи на экране

| Подпись | Свойство | Значение по умолчанию |
|---------|----------|----------------------|
| Артикул | `ArticleCaption` | `Артикул заказа` |
| Статус | `StatusCaption` | `Статус заказа` |
| Адрес | `AddressCaption` | `Адрес пункта выдачи` |
| Дата заказа | `OrderDateCaption` | `Дата заказа` |
| Дата доставки | `DeliveryDateCaption` | `Дата доставки` |

### Размеры строки

| Свойство | По умолчанию |
|----------|--------------|
| `RowHeight` | `110` |
| `DeliveryColumnWidth` | `140` |

---

## Добавление, редактирование и удаление (CRUD)

Оба списка (`ScrollableDetailPanel`, `ScrollableOrderPanel`) поддерживают кнопки **Добавить / Изменить / Удалить** и клик по строке для редактирования. Формы добавления и редактирования открывает **ваше приложение** в обработчиках событий.

### Права по ролям

| Свойство | Описание | По умолчанию |
|----------|----------|--------------|
| `AllowAdd` | Показать кнопку добавления | `false` |
| `AllowEdit` | Показать кнопку изменения и открывать редактирование по клику на строку | `false` |
| `AllowDelete` | Показать кнопку удаления | `false` |
| `IsEditSessionActive` | Блокирует второе окно редактирования, пока `true` | `false` |
| `ValidateBeforeDelete` | Функция проверки перед удалением; верните текст ошибки для запрета | `null` |

При `false` соответствующая кнопка **скрывается**. При попытке действия без прав показывается `MessageBox` с пояснением.

### События

| Событие | Когда срабатывает |
|---------|-------------------|
| `RecordAddRequested` | Нажата кнопка «Добавить» |
| `RecordEditRequested` | Клик по строке или кнопка «Изменить» |
| `RecordDeleting` | Перед удалением; установите `Cancel = true` и `DenyReason` для отмены |
| `RecordDeleted` | Строка удалена из `DataTable` (вызовите `Update` в БД) |

После закрытия формы редактирования вызовите `EndEditSession()`, чтобы снова можно было открыть другое окно.

### Пример для администратора (товары)

```csharp
var panel = new ScrollableDetailPanel { Dock = DockStyle.Fill };
panel.AllowAdd = panel.AllowEdit = panel.AllowDelete = userIsAdmin;
panel.ShowQueryPanel = userIsManagerOrAdmin;

panel.ValidateBeforeDelete = row =>
{
    var article = row["Артикул"]?.ToString();
    return ProductExistsInOrders(article)
        ? "Товар присутствует в заказе и не может быть удалён."
        : null;
};

panel.RecordAddRequested += (_, __) => OpenProductForm(null);
panel.RecordEditRequested += (_, e) => OpenProductForm(e.Row);
panel.RecordDeleted += (_, e) =>
{
    tableAdapter.Update(dataSet);
    panel.RefreshRows();
};
```

### Пример для менеджера (заказы — только просмотр)

```csharp
ordersPanel.AllowAdd = ordersPanel.AllowEdit = ordersPanel.AllowDelete = userIsAdmin;
// менеджер: все три свойства false — кнопки скрыты, клик только выделяет строку
```

### Обработка исключительных случаев

Библиотека показывает `MessageBox` в ситуациях:

- действие недоступно для роли;
- не выбрана запись для изменения или удаления;
- уже открыто окно редактирования (`IsEditSessionActive`);
- `ValidateBeforeDelete` или `RecordDeleting` запретили удаление;
- ошибка при удалении строки из `DataTable`.

---

## BoundDataGridView — настраиваемые свойства

| Свойство | Описание | По умолчанию |
|----------|----------|--------------|
| `HighlightColumnName` | Столбец для подсветки ячеек | `Действующая скидка` |
| `ApplyRowHighlight` | Включить подсветку | `true` |
| `Appearance` | Палитра цветов | см. ниже |

---

## AppearancePalette — настройка цветов и шрифтов

Общий объект `Appearance` доступен у всех трёх контролов.

### Редактор в дизайнере Visual Studio

1. Выберите контрол на форме.
2. В окне **Свойства** найдите `Appearance`.
3. **Разверните** узел — каждый цвет редактируется стандартным выбором цвета.
4. Нажмите **«…»** рядом с `Appearance` — откроется окно **«Палитра оформления»** со всеми цветами, шрифтами и порогом скидки; кнопка **«По умолчанию»** сбрасывает значения.

Изменения сразу применяются к контролу на форме в режиме дизайна.

| Свойство | Назначение | По умолчанию |
|----------|------------|--------------|
| `MainBackground` | Основной фон | `#FFFFFF` |
| `SectionBackground` | Фон заголовков таблицы | `#F5DEB3` |
| `AccentBackground` | Линии сетки таблицы | `#DEB887` |
| `HighlightBackground` | Подсветка скидки > порога | `#FFDEAD` |
| `SelectedRowBackground` | Фон выбранной строки списка | `#E8EEF8` |
| `PrimaryText` | Цвет текста | `Black` |
| `SeparatorText` | Цвет разделителя `\|` | `Gray` |
| `BorderColor` | Рамки строк списка | `Black` |
| `RowSeparatorColor` | Линия между записями | `Silver` |
| `OutOfStockBackground` | Фон строки без товара на складе | `#ADD8E6` |
| `StrikethroughPriceColor` | Перечёркнутая цена при скидке | `Red` |
| `FinalPriceColor` | Итоговая цена при скидке | `Black` |
| `FontFamily` | Шрифт | `Arial` |
| `BaseFontSize` | Размер обычного текста | `9` |
| `TitleFontSize` | Размер заголовка строки | `10` |
| `HighlightThresholdPercent` | Порог подсветки скидки (%) | `17` |

---

## Алгоритм расчёта скидки

### Назначение

Вычисление **итоговой цены товара** с учётом действующей скидки и определение **способа отображения** цены на экране (`ScrollableDetailPanel`, `BoundDataGridView`).

### Входные данные

| Обозначение | Описание | Источник |
|-------------|----------|----------|
| `Ц` | Исходная цена товара | столбец `Цена` (`PriceField`) |
| `С` | Действующая скидка, % | столбец `Действующая скидка` (`DiscountField`) |
| `П` | Порог подсветки скидки, % | `Appearance.HighlightThresholdPercent` (по умолчанию 17) |

### Выходные данные

| Обозначение | Описание |
|-------------|----------|
| `Ц_итог` | Итоговая цена (руб., 2 знака после запятой) |
| `Режим` | Способ отображения: «только цена» или «перечёркнутая + итоговая» |
| `Подсветка` | Применять ли фон подсветки для ячейки/блока скидки |

### Формула

Если `Ц > 0` и `С > 0`:

```
Ц_итог = ОКРУГЛ( Ц × (1 − С / 100) ; 2 )
```

Иначе:

```
Ц_итог = Ц
```

Округление — **математическое**, до 2 знаков (`MidpointRounding.AwayFromZero`).

### Обозначения блок-схемы (ГОСТ 19.701-90)

| Символ | Наименование по ГОСТ | Назначение в схеме |
|--------|----------------------|--------------------|
| ◯ (овал) | **Терминатор** | Начало / конец алгоритма |
| ▱ (параллелограмм) | **Данные** | Ввод исходных значений `Ц`, `С`, `П` |
| ▭ (прямоугольник) | **Процесс** | Вычисление, присваивание, форматирование |
| ◇ (ромб) | **Решение** | Проверка условия (да / нет) |

### Блок-схема алгоритма (ГОСТ 19.701-90)

В диаграмме Mermaid: овал — терминатор, параллелограмм — ввод/вывод, прямоугольник — процесс, ромб — решение.

```mermaid
flowchart TD
    A([НАЧАЛО]) --> B[/Ввод: Ц — цена, С — скидка %, П — порог подсветки/]
    B --> C{Ц ≤ 0<br/>ИЛИ<br/>С ≤ 0?}
    C -- ДА --> D[Ц_итог := Ц]
    C -- НЕТ --> E[Ц_итог := ОКРУГЛ Ц × 1 − С / 100 ; 2]
    D --> F{С > 0<br/>И<br/>Ц > 0?}
    E --> F
    F -- ДА --> G[Режим := перечёркнутая Ц + итоговая Ц_итог]
    F -- НЕТ --> H[Режим := отображать только Ц]
    G --> I{С > П?}
    H --> I
    I -- ДА --> J[Подсветка := включить HighlightBackground]
    I -- НЕТ --> K[Подсветка := обычный фон]
    J --> L[/Вывод: Ц_итог, Режим, Подсветка/]
    K --> L
    L --> M([КОНЕЦ])
```

### Блок-схема в текстовом виде (ГОСТ 19.701-90)

```
        ┌─────────────────────────────────┐
        │            НАЧАЛО               │  ← терминатор (овал)
        └───────────────┬─────────────────┘
                        ▼
        ╔═════════════════════════════════╗
        ║  Ввод: Ц, С, П                  ║  ← данные (параллелограмм)
        ╚═════════════════════════════════╝
                        ▼
                ┌───────────────┐
                │  Ц ≤ 0  ИЛИ   │
                │  С ≤ 0 ?      │              ← решение (ромб)
                └───┬───────┬───┘
               ДА   │       │   НЕТ
                    ▼       ▼
        ┌───────────────┐   ┌───────────────────────────────┐
        │ Ц_итог := Ц   │   │ Ц_итог := ОКРУГЛ(Ц×(1−С/100);2)│  ← процесс
        └───────┬───────┘   └───────────────┬───────────────┘
                └───────────┬───────────────┘
                            ▼
                    ┌───────────────┐
                    │ С > 0  И      │
                    │ Ц > 0 ?       │              ← решение (ромб)
                    └───┬───────┬───┘
                   ДА   │       │   НЕТ
                        ▼       ▼
        ┌───────────────────────┐   ┌─────────────────────────┐
        │ Режим := перечёркнутая│   │ Режим := только Ц       │
        │ цена + Ц_итог         │   └───────────┬─────────────┘
        └───────────┬───────────┘               │
                    └───────────┬───────────────┘
                                ▼
                        ┌───────────────┐
                        │   С > П ?     │          ← решение (ромб)
                        └───┬───────┬───┘
                       ДА   │       │   НЕТ
                            ▼       ▼
        ┌───────────────────────┐   ┌─────────────────────────┐
        │ Подсветка := ДА       │   │ Подсветка := НЕТ        │
        └───────────┬───────────┘   └───────────┬─────────────┘
                    └───────────┬───────────────┘
                                ▼
        ╔═════════════════════════════════╗
        ║  Вывод: Ц_итог, Режим,          ║  ← данные (параллелограмм)
        ║         Подсветка               ║
        ╚═════════════════════════════════╝
                                ▼
        ┌─────────────────────────────────┐
        │             КОНЕЦ                │  ← терминатор (овал)
        └─────────────────────────────────┘
```

### Таблица решений (ветвления)

| № | Условие | Действие |
|---|---------|----------|
| 1 | `Ц ≤ 0` **или** `С ≤ 0` | `Ц_итог = Ц` (скидка не применяется) |
| 2 | `Ц > 0` **и** `С > 0` | `Ц_итог = ОКРУГЛ(Ц × (1 − С/100); 2)` |
| 3 | `С > 0` **и** `Ц > 0` | Показать старую цену **красной перечёркнутой** и рядом **итоговую чёрную** |
| 4 | иначе | Показать только `Ц` |
| 5 | `С > П` | Подсветить ячейку/блок скидки цветом `HighlightBackground` |
| 6 | `С ≤ П` | Обычный фон |

### Примеры расчёта

| Ц, руб. | С, % | Ц_итог, руб. | Отображение |
|---------|------|--------------|-------------|
| 1000 | 0 | 1000,00 | `1000,00` |
| 1000 | 15 | 850,00 | ~~1000,00~~ **850,00** |
| 1000 | 20 | 800,00 | ~~1000,00~~ **800,00** + подсветка (20 > 17) |
| 0 | 10 | 0,00 | `0,00` (скидка не считается) |
| 500 | −5 | 500,00 | `500,00` (отрицательная скидка игнорируется) |

### Соответствие коду библиотеки

| Шаг алгоритма | Реализация |
|---------------|------------|
| Расчёт `Ц_итог` | `ScrollableDetailPanel.CalculateFinalPrice` |
| Отображение цены в строке | `ProductListRow` → `PriceLinePanel` |
| Подсветка скидки в списке | `Appearance.HighlightThresholdPercent` |
| Подсветка в таблице | `BoundDataGridView.OnCellFormatting` |

---

## Примеры использования

### Список товаров с привязкой к DataSet

```csharp
using System.Data;
using System.Drawing;
using AlfaToad.CustomControls.Controls;
using AlfaToad.CustomControls.Theme;

var panel = new ScrollableDetailPanel { Dock = DockStyle.Fill };

panel.NoImage = Image.FromFile(@"C:\images\no-photo.png");
panel.ImagesFolder = @"C:\images\products";

panel.CategoryField = "CategoryName";
panel.NameField = "ProductName";
panel.DiscountField = "DiscountPercent";

panel.Appearance = new AppearancePalette
{
    MainBackground = Color.White,
    HighlightBackground = Color.FromArgb(255, 222, 173),
    BorderColor = Color.Black,
    HighlightThresholdPercent = 17m
};

DataSet dataSet = LoadProducts();
panel.BindDataSet(dataSet, "Products");
```

**Поведение цены:** если `Действующая скидка` > 0, основная цена отображается красной и перечёркнутой, рядом — итоговая цена чёрным (`цена × (1 − скидка/100)`).

**Нет на складе:** если `Кол-во на складе` ≤ 0, вся строка подсвечивается цветом `OutOfStockRowColor`.

### Поиск, фильтрация и сортировка (`ScrollableDetailPanel`)

Панель включена по умолчанию (`ShowQueryPanel = true`). Всё применяется **в реальном времени** без кнопки «Найти».

| Элемент | Поведение |
|---------|-----------|
| **Поиск** | Подстрока по всем текстовым столбцам (`string`), одновременно по нескольким полям (логика OR) |
| **Сортировка** | Цена ↑/↓, кол-во на складе ↑/↓; выбор сохраняется при поиске и фильтре |
| **Поставщик** | Выпадающий список; первый пункт `Все поставщики` сбрасывает фильтр |

| Свойство | Описание |
|----------|----------|
| `ShowQueryPanel` | Показать панель (по умолчанию `true`) |
| `AllSuppliersCaption` | Текст сброса фильтра (`Все поставщики`) |
| `SearchFields` | Ограничить столбцы поиска (пусто = все текстовые) |
| `SupplierField` | Столбец для фильтра поставщика (`Поставщик`) |
| `PriceField` / `StockField` | Столбцы для сортировки |

```csharp
panel.ShowQueryPanel = userIsManagerOrAdmin;
```

После `TableAdapter.Fill(...)` список и список поставщиков обновляются автоматически.

### Список заказов

```csharp
var orders = new ScrollableOrderPanel { Dock = DockStyle.Fill };
orders.Appearance = AppearancePalette.CreateDefault();
orders.Appearance.AccentBackground = Color.BurlyWood;
orders.BindDataSet(dataSet, "Orders");
```

### Таблица с подсветкой скидки

```csharp
var grid = new BoundDataGridView { Dock = DockStyle.Fill };
grid.Appearance = AppearancePalette.CreateDefault();
grid.HighlightColumnName = "Действующая скидка";
grid.ApplyDefaultStyle();
grid.BindDataSet(dataSet, "Products");
```

### Смена цветов после создания

```csharp
panel.Appearance.MainBackground = ColorTranslator.FromHtml("#FAFAFA");
panel.Appearance.HighlightBackground = ColorTranslator.FromHtml("#FFDEAD");
panel.Appearance.BorderColor = Color.DimGray;
panel.ApplyAppearance();
```
