392 lines
16 KiB
C#
392 lines
16 KiB
C#
using ComponentOrientedPlatform.Abstractions;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Configuration;
|
||
using System.Reflection;
|
||
using WinFormsComponentOrientedHost.Data;
|
||
using WinFormsComponentOrientedHost.Data.Entities;
|
||
|
||
public sealed class FormExtensions : Form
|
||
{
|
||
public static IConfiguration Configuration { get; private set; } = default!;
|
||
private readonly IHostServices _host;
|
||
|
||
private readonly ComboBox _cbText = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
|
||
private readonly Button _btnText = new() { Text = "Excel (большой текст: в наличии)", Dock = DockStyle.Fill };
|
||
|
||
private readonly ComboBox _cbLine = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
|
||
private readonly Button _btnLine = new() { Text = "Excel (диаграмма: нет в наличии)", Dock = DockStyle.Fill };
|
||
|
||
private readonly ComboBox _cbRowCol = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
|
||
private readonly Button _btnRowCol = new() { Text = "Excel (таблица: 1-я строка + 1-й столбец)", Dock = DockStyle.Fill };
|
||
|
||
private readonly ComboBox _cbPiePdf = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
|
||
private readonly Button _btnPiePdf = new() { Text = "PDF (круговая: нет в наличии)", Dock = DockStyle.Fill };
|
||
|
||
private readonly List<IReportDocumentWithContextTextsContract> _textPlugins = new();
|
||
private readonly List<IReportDocumentWithChartLineContract> _linePlugins = new();
|
||
private readonly List<IReportDocumentWithTableColumnRowHeaderContract> _rowColPlugins = new();
|
||
private readonly List<IReportDocumentWithChartPieContract> _piePdfPlugins = new();
|
||
|
||
public FormExtensions(IHostServices host)
|
||
{
|
||
_host = host;
|
||
|
||
Text = "Отчёты → Excel/PDF";
|
||
Width = 860;
|
||
Height = 280;
|
||
StartPosition = FormStartPosition.CenterParent;
|
||
|
||
var layout = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 2,
|
||
RowCount = 4,
|
||
AutoSize = true,
|
||
Padding = new Padding(12),
|
||
};
|
||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60));
|
||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40));
|
||
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
||
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
||
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
||
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
||
|
||
layout.Controls.Add(_cbText, 0, 0);
|
||
layout.Controls.Add(_btnText, 1, 0);
|
||
layout.Controls.Add(_cbLine, 0, 1);
|
||
layout.Controls.Add(_btnLine, 1, 1);
|
||
layout.Controls.Add(_cbRowCol, 0, 2);
|
||
layout.Controls.Add(_btnRowCol, 1, 2);
|
||
layout.Controls.Add(_cbPiePdf, 0, 3);
|
||
layout.Controls.Add(_btnPiePdf, 1, 3);
|
||
|
||
Controls.Add(layout);
|
||
|
||
Load += (_, __) => InitializeAndLoadPlugins();
|
||
|
||
_btnText.Click += async (_, __) => await GenerateExcelBigTextReport();
|
||
_btnLine.Click += async (_, __) => await GenerateExcelOutOfStockLine();
|
||
_btnRowCol.Click += async (_, __) => await GenerateExcelRowColHeaderTable();
|
||
_btnPiePdf.Click += async (_, __) => await GeneratePdfOutOfStockPie();
|
||
}
|
||
|
||
private void InitializeAndLoadPlugins()
|
||
{
|
||
Configuration = new ConfigurationBuilder()
|
||
.SetBasePath(AppContext.BaseDirectory)
|
||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
|
||
.Build();
|
||
|
||
LoadPlugins();
|
||
}
|
||
|
||
private void LoadPlugins()
|
||
{
|
||
_cbText.Items.Clear();
|
||
_cbLine.Items.Clear();
|
||
_cbRowCol.Items.Clear();
|
||
_cbPiePdf.Items.Clear();
|
||
|
||
_textPlugins.Clear();
|
||
_linePlugins.Clear();
|
||
_rowColPlugins.Clear();
|
||
_piePdfPlugins.Clear();
|
||
|
||
var path = Path.Combine(AppContext.BaseDirectory, "Plugins");
|
||
if (!Directory.Exists(path))
|
||
{
|
||
MessageBox.Show(this, $"Папка с плагинами не найдена:\n{path}", "Расширения",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
foreach (var file in Directory.GetFiles(path, "*.dll"))
|
||
{
|
||
try
|
||
{
|
||
var asm = Assembly.LoadFrom(file);
|
||
foreach (var type in asm.GetTypes())
|
||
{
|
||
if (type.IsAbstract || type.IsInterface) continue;
|
||
|
||
if (typeof(IReportDocumentWithContextTextsContract).IsAssignableFrom(type) &&
|
||
Activator.CreateInstance(type) is IReportDocumentWithContextTextsContract t1)
|
||
{
|
||
_textPlugins.Add(t1);
|
||
_cbText.Items.Add($"{type.Name} ({t1.DocumentFormat})");
|
||
}
|
||
|
||
if (typeof(IReportDocumentWithChartLineContract).IsAssignableFrom(type) &&
|
||
Activator.CreateInstance(type) is IReportDocumentWithChartLineContract t2)
|
||
{
|
||
_linePlugins.Add(t2);
|
||
_cbLine.Items.Add($"{type.Name} ({t2.DocumentFormat})");
|
||
}
|
||
|
||
if (typeof(IReportDocumentWithTableColumnRowHeaderContract).IsAssignableFrom(type) &&
|
||
Activator.CreateInstance(type) is IReportDocumentWithTableColumnRowHeaderContract t3)
|
||
{
|
||
_rowColPlugins.Add(t3);
|
||
_cbRowCol.Items.Add($"{type.Name} ({t3.DocumentFormat})");
|
||
}
|
||
|
||
if (typeof(IReportDocumentWithChartPieContract).IsAssignableFrom(type) &&
|
||
Activator.CreateInstance(type) is IReportDocumentWithChartPieContract t4)
|
||
{
|
||
_piePdfPlugins.Add(t4);
|
||
_cbPiePdf.Items.Add($"{type.Name} ({t4.DocumentFormat})");
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
if (_cbText.Items.Count > 0) _cbText.SelectedIndex = 0;
|
||
if (_cbLine.Items.Count > 0) _cbLine.SelectedIndex = 0;
|
||
if (_cbRowCol.Items.Count > 0) _cbRowCol.SelectedIndex = 0;
|
||
if (_cbPiePdf.Items.Count > 0) _cbPiePdf.SelectedIndex = 0;
|
||
}
|
||
|
||
private async Task GenerateExcelBigTextReport()
|
||
{
|
||
if (_cbText.SelectedIndex < 0) return;
|
||
var plugin = _textPlugins[_cbText.SelectedIndex];
|
||
|
||
try
|
||
{
|
||
var cs = Configuration["Database:ConnectionString"]
|
||
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
|
||
|
||
var dbFactory = new AppDbContextFactory(cs);
|
||
using var db = dbFactory.CreateDbContext();
|
||
|
||
var productsInStock = await db.Set<Product>()
|
||
.AsNoTracking()
|
||
.Where(p => (p.quantity ?? 0) > 0)
|
||
.OrderBy(p => p.categoryText)
|
||
.ThenBy(p => p.name)
|
||
.ToListAsync();
|
||
|
||
if (productsInStock.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "Нет продуктов в наличии.", "Excel (большой текст)",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var paragraphs = productsInStock.Select(p =>
|
||
{
|
||
var cat = string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim();
|
||
var name = (p.name ?? "").Trim();
|
||
var desc = string.IsNullOrWhiteSpace(p.description) ? "(описание отсутствует)" : p.description!.Trim();
|
||
return $"[{cat}] {name} — {desc}";
|
||
}).ToList();
|
||
|
||
using var dlg = new SaveFileDialog
|
||
{
|
||
Filter = "Excel (*.xlsx)|*.xlsx",
|
||
FileName = "Продукты_в_наличии_текст.xlsx",
|
||
OverwritePrompt = true
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
await plugin.CreateDocumentAsync(
|
||
filePath: dlg.FileName,
|
||
header: "Продукты в наличии — сводный текст",
|
||
paragraphs: paragraphs);
|
||
|
||
MessageBox.Show(this, $"Excel создан. Строк: {paragraphs.Count}.",
|
||
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "Ошибка при генерации Excel:\n" + ex.Message,
|
||
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private async Task GenerateExcelOutOfStockLine()
|
||
{
|
||
if (_cbLine.SelectedIndex < 0) return;
|
||
var plugin = _linePlugins[_cbLine.SelectedIndex];
|
||
|
||
try
|
||
{
|
||
var cs = Configuration["Database:ConnectionString"]
|
||
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
|
||
|
||
var dbFactory = new AppDbContextFactory(cs);
|
||
using var db = dbFactory.CreateDbContext();
|
||
|
||
var byCategory = await db.Set<Product>()
|
||
.AsNoTracking()
|
||
.GroupBy(p => string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim())
|
||
.Select(g => new
|
||
{
|
||
Category = g.Key,
|
||
OutOfStock = g.Count(p => (p.quantity ?? 0) <= 0)
|
||
})
|
||
.Where(x => x.OutOfStock > 0)
|
||
.OrderByDescending(x => x.OutOfStock)
|
||
.ToListAsync();
|
||
|
||
if (byCategory.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "Все категории в наличии — нечего показывать.",
|
||
"Excel (вертикальные столбцы)", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var series = new Dictionary<string, List<(int Parameter, double Value)>>
|
||
{
|
||
["Нет в наличии"] = byCategory.Select((x, i) => (Parameter: i + 1, Value: (double)x.OutOfStock)).ToList()
|
||
};
|
||
|
||
using var dlg = new SaveFileDialog
|
||
{
|
||
Filter = "Excel (*.xlsx)|*.xlsx",
|
||
FileName = "Продукты_нет_в_наличии_по_категориям_столбцы.xlsx",
|
||
OverwritePrompt = true
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
var header = "Отсутствующие товары по категориям";
|
||
var chartTitle = "Количество отсутствующих товаров (по категориям)";
|
||
|
||
await plugin.CreateDocumentAsync(dlg.FileName, header, chartTitle, series);
|
||
|
||
MessageBox.Show(this, "Excel с диаграммой создан.",
|
||
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "Ошибка при генерации Excel-диаграммы:\n" + ex.Message,
|
||
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private async Task GenerateExcelRowColHeaderTable()
|
||
{
|
||
if (_cbRowCol.SelectedIndex < 0) return;
|
||
var plugin = _rowColPlugins[_cbRowCol.SelectedIndex];
|
||
|
||
try
|
||
{
|
||
var cs = Configuration["Database:ConnectionString"]
|
||
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
|
||
|
||
var dbFactory = new AppDbContextFactory(cs);
|
||
using var db = dbFactory.CreateDbContext();
|
||
|
||
var items = await db.Set<Product>()
|
||
.AsNoTracking()
|
||
.OrderBy(p => p.name)
|
||
.ToListAsync();
|
||
|
||
if (items.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "Нет данных для отчёта.", "Excel (таблица)",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var rowsHeights = new List<int> { 25, 25 };
|
||
var headers = new List<(string Header, string PropertyName, string FiledName)>
|
||
{
|
||
("Id", "id", "Id"),
|
||
("Название", "name", "Name"),
|
||
("Категория", "categoryText", "Category"),
|
||
("Количество", "quantity", "Quantity"),
|
||
};
|
||
|
||
using var dlg = new SaveFileDialog
|
||
{
|
||
Filter = "Excel (*.xlsx)|*.xlsx",
|
||
FileName = "Продукты_таблица_две_строки.xlsx",
|
||
OverwritePrompt = true
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
var columnsWidth = new List<int>();
|
||
bool isHeaderFirstRow = true;
|
||
|
||
await plugin.CreateDocumentAsync(
|
||
filePath: dlg.FileName,
|
||
header: "Продукты — шапка: первые два столбца",
|
||
columnsWidth: columnsWidth,
|
||
rowsHeights: rowsHeights,
|
||
isHeaderFirstRow: isHeaderFirstRow,
|
||
headers: headers,
|
||
data: items);
|
||
|
||
MessageBox.Show(this, "Excel-таблица создана.",
|
||
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "Ошибка при генерации Excel-таблицы:\n" + ex.Message,
|
||
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
|
||
private async Task GeneratePdfOutOfStockPie()
|
||
{
|
||
if (_cbPiePdf.SelectedIndex < 0) return;
|
||
var plugin = _piePdfPlugins[_cbPiePdf.SelectedIndex];
|
||
|
||
try
|
||
{
|
||
var cs = Configuration["Database:ConnectionString"]
|
||
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
|
||
|
||
var dbFactory = new AppDbContextFactory(cs);
|
||
using var db = dbFactory.CreateDbContext();
|
||
|
||
var byCategory = await db.Set<Product>()
|
||
.AsNoTracking()
|
||
.GroupBy(p => string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim())
|
||
.Select(g => new
|
||
{
|
||
Category = g.Key,
|
||
OutOfStock = g.Count(p => (p.quantity ?? 0) <= 0)
|
||
})
|
||
.Where(x => x.OutOfStock > 0)
|
||
.OrderByDescending(x => x.OutOfStock)
|
||
.ToListAsync();
|
||
|
||
if (byCategory.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "Все категории в наличии — круговую диаграмму строить не из чего.",
|
||
"PDF (круговая)", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var series = byCategory
|
||
.Select((x, i) => (Parameter: i + 1, Value: (double)x.OutOfStock))
|
||
.ToList();
|
||
|
||
using var dlg = new SaveFileDialog
|
||
{
|
||
Filter = "PDF (*.pdf)|*.pdf",
|
||
FileName = "Продукты_нет_в_наличии_по_категориям_круговая.pdf",
|
||
OverwritePrompt = true
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
var header = "Отсутствующие товары по категориям";
|
||
var chartTitle = "Круговая диаграмма: количество отсутствующих товаров";
|
||
|
||
await plugin.CreateDocumentAsync(dlg.FileName, header, chartTitle, series);
|
||
|
||
MessageBox.Show(this, "PDF с круговой диаграммой создан.",
|
||
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "Ошибка при генерации PDF-диаграммы:\n" + ex.Message,
|
||
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
}
|