Сделал первое требование
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
using TwoFromTheCasketContracts.BusinessLogicsContracts;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
using TwoFromTheCasketContracts.Exceptions;
|
||||
using TwoFromTheCasketContracts.StorageContracts;
|
||||
|
||||
namespace TwoFromTheCasketBusinessLogic.Implementation;
|
||||
|
||||
internal class ReportContract(
|
||||
IRoomStorageContract roomStorageContract, IRoomHistoryStorageContract roomHistoryStorageContract,
|
||||
ILogger logger, BaseWordBuilder baseWordBuilder,
|
||||
BaseExcelBuilder baseExcelBuilder, BasePdfBuilder basePdfBuilder) : IReportContract
|
||||
{
|
||||
private readonly IRoomStorageContract _roomStorageContract = roomStorageContract;
|
||||
private readonly IRoomHistoryStorageContract _roomHistoryStorageContract = roomHistoryStorageContract;
|
||||
private readonly ILogger _logger = logger;
|
||||
private readonly BaseWordBuilder _baseWordBuilder = baseWordBuilder;
|
||||
private readonly BaseExcelBuilder _baseExcelBuilder = baseExcelBuilder;
|
||||
private readonly BasePdfBuilder _basePdfBuilder = basePdfBuilder;
|
||||
|
||||
public async Task<List<RoomWithHistoryDataModel>> GetRoomHistoryGroupedAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("Get report: room with history");
|
||||
|
||||
var rooms = await _roomStorageContract.GetListAsync(ct);
|
||||
var history = await _roomHistoryStorageContract.GetListAsync(ct);
|
||||
|
||||
var result = rooms.Select(room => new RoomWithHistoryDataModel
|
||||
{
|
||||
Room = room,
|
||||
History = history
|
||||
.Where(h => h.RoomId == room.Id)
|
||||
.OrderBy(h => h.DateChange)
|
||||
.ToList()
|
||||
}).ToList();
|
||||
|
||||
_logger.LogInformation("Report finished. Count: {Count}", result.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Stream> CreateDocumentRoomHistoryAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("Create report: RoomHistory grouped");
|
||||
|
||||
var data = await GetRoomHistoryGroupedAsync(ct)
|
||||
?? throw new InvalidOperationException("No found data");
|
||||
|
||||
var header = new List<string[]> { new[] { "RoomId", "OwnerFIO", "Address", "Type", "DateChanged" } };
|
||||
|
||||
var rows = data.SelectMany(roomGroup =>
|
||||
{
|
||||
var baseRow = new[] { roomGroup.Room.Id, roomGroup.Room.OwnerFIO, roomGroup.Room.Address, roomGroup.Room.Type.ToString(), "" };
|
||||
|
||||
var historyRows = roomGroup.History.Select(h =>
|
||||
new[] { "", h.OwnerFIO, "", h.Type.ToString(), h.DateChange.ToString("yyyy-MM-dd HH:mm") });
|
||||
|
||||
return new List<string[]> { baseRow }.Union(historyRows);
|
||||
}).ToList();
|
||||
|
||||
return _baseWordBuilder
|
||||
.AddHeader("История изменений помещений")
|
||||
.AddParagraph($"Сформировано: {DateTime.Now:dd.MM.yyyy HH:mm}")
|
||||
.AddTable([20, 20, 30, 15, 20], header.Concat(rows).ToList())
|
||||
.Build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public abstract class BaseExcelBuilder
|
||||
{
|
||||
public abstract BaseExcelBuilder AddHeader(string header, int startIndex, int count);
|
||||
|
||||
public abstract BaseExcelBuilder AddParagraph(string text, int columnIndex);
|
||||
|
||||
public abstract BaseExcelBuilder AddTable(int[] columnsWidths, List<string[]> data);
|
||||
|
||||
public abstract Stream Build();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public abstract class BasePdfBuilder
|
||||
{
|
||||
public abstract BasePdfBuilder AddHeader(string header);
|
||||
|
||||
public abstract BasePdfBuilder AddParagraph(string text);
|
||||
|
||||
public abstract BasePdfBuilder AddPieChart(string title, List<(string Caption, double Value)> data);
|
||||
|
||||
public abstract Stream Build();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public abstract class BaseWordBuilder
|
||||
{
|
||||
public abstract BaseWordBuilder AddHeader(string header);
|
||||
public abstract BaseWordBuilder AddParagraph(string text);
|
||||
public abstract BaseWordBuilder AddTable(int[] widths, List<string[]> data);
|
||||
public abstract Stream Build();
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using MigraDoc.DocumentObjectModel;
|
||||
using MigraDoc.DocumentObjectModel.Shapes.Charts;
|
||||
using MigraDoc.Rendering;
|
||||
using System.Text;
|
||||
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public class MigraDocPdfBuilder : BasePdfBuilder
|
||||
{
|
||||
private readonly Document _document;
|
||||
|
||||
public MigraDocPdfBuilder()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
_document = new Document();
|
||||
DefineStyles();
|
||||
}
|
||||
|
||||
public override BasePdfBuilder AddHeader(string header)
|
||||
{
|
||||
_document.AddSection().AddParagraph(header, "NormalBold");
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BasePdfBuilder AddParagraph(string text)
|
||||
{
|
||||
_document.LastSection.AddParagraph(text, "Normal");
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BasePdfBuilder AddPieChart(string title, List<(string Caption, double Value)> data)
|
||||
{
|
||||
if (data == null || data.Count == 0)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var chart = new Chart(ChartType.Pie2D);
|
||||
var series = chart.SeriesCollection.AddSeries();
|
||||
series.Add(data.Select(x => x.Value).ToArray());
|
||||
|
||||
var xseries = chart.XValues.AddXSeries();
|
||||
xseries.Add(data.Select(x => x.Caption).ToArray());
|
||||
|
||||
chart.DataLabel.Type = DataLabelType.Percent;
|
||||
chart.DataLabel.Position = DataLabelPosition.OutsideEnd;
|
||||
|
||||
chart.Width = Unit.FromCentimeter(16);
|
||||
chart.Height = Unit.FromCentimeter(12);
|
||||
|
||||
chart.TopArea.AddParagraph(title);
|
||||
|
||||
chart.XAxis.MajorTickMark = TickMarkType.Outside;
|
||||
|
||||
chart.YAxis.MajorTickMark = TickMarkType.Outside;
|
||||
chart.YAxis.HasMajorGridlines = true;
|
||||
|
||||
chart.PlotArea.LineFormat.Width = 1;
|
||||
chart.PlotArea.LineFormat.Visible = true;
|
||||
|
||||
chart.TopArea.AddLegend();
|
||||
|
||||
_document.LastSection.Add(chart);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override Stream Build()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var renderer = new PdfDocumentRenderer()
|
||||
{
|
||||
Document = _document
|
||||
};
|
||||
renderer.RenderDocument();
|
||||
renderer.PdfDocument.Save(stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
private void DefineStyles()
|
||||
{
|
||||
var style = _document.Styles.AddStyle("NormalBold", "Normal");
|
||||
style.Font.Name = "Arial";
|
||||
style.Font.Bold = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public class OpenXmlExcelBuilder : BaseExcelBuilder
|
||||
{
|
||||
private readonly SheetData _sheetData;
|
||||
|
||||
private readonly MergeCells _mergeCells;
|
||||
|
||||
private readonly Columns _columns;
|
||||
|
||||
private uint _rowIndex = 0;
|
||||
|
||||
public OpenXmlExcelBuilder()
|
||||
{
|
||||
_sheetData = new SheetData();
|
||||
_mergeCells = new MergeCells();
|
||||
_columns = new Columns();
|
||||
_rowIndex = 1;
|
||||
}
|
||||
|
||||
public override BaseExcelBuilder AddHeader(string header, int startIndex, int count)
|
||||
{
|
||||
CreateCell(startIndex, _rowIndex, header, StyleIndex.BoldTextWithoutBorder);
|
||||
for (int i = startIndex + 1; i < startIndex + count; ++i)
|
||||
{
|
||||
CreateCell(i, _rowIndex, "", StyleIndex.SimpleTextWithoutBorder);
|
||||
}
|
||||
|
||||
_mergeCells.Append(new MergeCell()
|
||||
{
|
||||
Reference =
|
||||
new StringValue($"{GetExcelColumnName(startIndex)}{_rowIndex}:{GetExcelColumnName(startIndex + count - 1)}{_rowIndex}")
|
||||
});
|
||||
|
||||
_rowIndex++;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BaseExcelBuilder AddParagraph(string text, int columnIndex)
|
||||
{
|
||||
CreateCell(columnIndex, _rowIndex++, text, StyleIndex.SimpleTextWithoutBorder);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BaseExcelBuilder AddTable(int[] columnsWidths, List<string[]> data)
|
||||
{
|
||||
if (columnsWidths == null || columnsWidths.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columnsWidths));
|
||||
}
|
||||
|
||||
if (data == null || data.Count == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
if (data.Any(x => x.Length != columnsWidths.Length))
|
||||
{
|
||||
throw new InvalidOperationException("widths.Length != data.Length");
|
||||
}
|
||||
|
||||
uint counter = 1;
|
||||
int coef = 2;
|
||||
_columns.Append(columnsWidths.Select(x => new Column
|
||||
{
|
||||
Min = counter,
|
||||
Max = counter++,
|
||||
Width = x * coef,
|
||||
CustomWidth = true
|
||||
}));
|
||||
|
||||
for (var j = 0; j < data.First().Length; ++j)
|
||||
{
|
||||
CreateCell(j, _rowIndex, data[0][j], StyleIndex.BoldTextWithBorder);
|
||||
}
|
||||
|
||||
_rowIndex++;
|
||||
for (var i = 1; i < data.Count - 1; ++i)
|
||||
{
|
||||
for (var j = 0; j < data[i].Length; ++j)
|
||||
{
|
||||
CreateCell(j, _rowIndex, data[i][j], StyleIndex.SimpleTextWithBorder);
|
||||
}
|
||||
|
||||
_rowIndex++;
|
||||
}
|
||||
|
||||
for (var j = 0; j < data.Last().Length; ++j)
|
||||
{
|
||||
CreateCell(j, _rowIndex, data.Last()[j], StyleIndex.SimpleTextWithBorder);
|
||||
}
|
||||
|
||||
_rowIndex++;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override Stream Build()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using var spreadsheetDocument = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook);
|
||||
var workbookpart = spreadsheetDocument.AddWorkbookPart();
|
||||
GenerateStyle(workbookpart);
|
||||
workbookpart.Workbook = new Workbook();
|
||||
var worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
|
||||
worksheetPart.Worksheet = new Worksheet();
|
||||
if (_columns.HasChildren)
|
||||
{
|
||||
worksheetPart.Worksheet.Append(_columns);
|
||||
}
|
||||
|
||||
worksheetPart.Worksheet.Append(_sheetData);
|
||||
var sheets = spreadsheetDocument.WorkbookPart!.Workbook.AppendChild(new Sheets());
|
||||
var sheet = new Sheet()
|
||||
{
|
||||
Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart),
|
||||
SheetId = 1,
|
||||
Name = "Лист 1"
|
||||
};
|
||||
|
||||
sheets.Append(sheet);
|
||||
if (_mergeCells.HasChildren)
|
||||
{
|
||||
worksheetPart.Worksheet.InsertAfter(_mergeCells, worksheetPart.Worksheet.Elements<SheetData>().First());
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static void GenerateStyle(WorkbookPart workbookPart)
|
||||
{
|
||||
var workbookStylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
|
||||
workbookStylesPart.Stylesheet = new Stylesheet();
|
||||
|
||||
var fonts = new Fonts() { Count = 2, KnownFonts = BooleanValue.FromBoolean(true) };
|
||||
fonts.Append(new DocumentFormat.OpenXml.Spreadsheet.Font
|
||||
{
|
||||
FontSize = new FontSize() { Val = 11 },
|
||||
FontName = new FontName() { Val = "Calibri" },
|
||||
FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
|
||||
FontScheme = new FontScheme() { Val = new EnumValue<FontSchemeValues>(FontSchemeValues.Minor) }
|
||||
});
|
||||
fonts.Append(new DocumentFormat.OpenXml.Spreadsheet.Font
|
||||
{
|
||||
FontSize = new FontSize() { Val = 11 },
|
||||
FontName = new FontName() { Val = "Calibri" },
|
||||
FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
|
||||
FontScheme = new FontScheme() { Val = new EnumValue<FontSchemeValues>(FontSchemeValues.Minor) },
|
||||
Bold = new Bold()
|
||||
});
|
||||
workbookStylesPart.Stylesheet.Append(fonts);
|
||||
|
||||
// Default Fill
|
||||
var fills = new Fills() { Count = 1 };
|
||||
fills.Append(new Fill
|
||||
{
|
||||
PatternFill = new PatternFill() { PatternType = new EnumValue<PatternValues>(PatternValues.None) }
|
||||
});
|
||||
workbookStylesPart.Stylesheet.Append(fills);
|
||||
|
||||
// Default Border
|
||||
var borders = new Borders() { Count = 2 };
|
||||
borders.Append(new Border
|
||||
{
|
||||
LeftBorder = new LeftBorder(),
|
||||
RightBorder = new RightBorder(),
|
||||
TopBorder = new TopBorder(),
|
||||
BottomBorder = new BottomBorder(),
|
||||
DiagonalBorder = new DiagonalBorder()
|
||||
});
|
||||
borders.Append(new Border
|
||||
{
|
||||
LeftBorder = new LeftBorder() { Style = BorderStyleValues.Thin },
|
||||
RightBorder = new RightBorder() { Style = BorderStyleValues.Thin },
|
||||
TopBorder = new TopBorder() { Style = BorderStyleValues.Thin },
|
||||
BottomBorder = new BottomBorder() { Style = BorderStyleValues.Thin }
|
||||
});
|
||||
workbookStylesPart.Stylesheet.Append(borders);
|
||||
|
||||
// Default cell format and a date cell format
|
||||
var cellFormats = new CellFormats() { Count = 4 };
|
||||
cellFormats.Append(new CellFormat
|
||||
{
|
||||
NumberFormatId = 0,
|
||||
FormatId = 0,
|
||||
FontId = 0,
|
||||
BorderId = 0,
|
||||
FillId = 0,
|
||||
Alignment = new Alignment()
|
||||
{
|
||||
Horizontal = HorizontalAlignmentValues.Left,
|
||||
Vertical = VerticalAlignmentValues.Center,
|
||||
WrapText = true
|
||||
}
|
||||
});
|
||||
cellFormats.Append(new CellFormat
|
||||
{
|
||||
NumberFormatId = 0,
|
||||
FormatId = 0,
|
||||
FontId = 0,
|
||||
BorderId = 1,
|
||||
FillId = 0,
|
||||
Alignment = new Alignment()
|
||||
{
|
||||
Horizontal = HorizontalAlignmentValues.Left,
|
||||
Vertical = VerticalAlignmentValues.Center,
|
||||
WrapText = true
|
||||
}
|
||||
});
|
||||
cellFormats.Append(new CellFormat
|
||||
{
|
||||
NumberFormatId = 0,
|
||||
FormatId = 0,
|
||||
FontId = 1,
|
||||
BorderId = 0,
|
||||
FillId = 0,
|
||||
Alignment = new Alignment()
|
||||
{
|
||||
Horizontal = HorizontalAlignmentValues.Center,
|
||||
Vertical = VerticalAlignmentValues.Center,
|
||||
WrapText = true
|
||||
}
|
||||
});
|
||||
cellFormats.Append(new CellFormat
|
||||
{
|
||||
NumberFormatId = 0,
|
||||
FormatId = 0,
|
||||
FontId = 1,
|
||||
BorderId = 1,
|
||||
FillId = 0,
|
||||
Alignment = new Alignment()
|
||||
{
|
||||
Horizontal = HorizontalAlignmentValues.Center,
|
||||
Vertical = VerticalAlignmentValues.Center,
|
||||
WrapText = true
|
||||
}
|
||||
});
|
||||
workbookStylesPart.Stylesheet.Append(cellFormats);
|
||||
}
|
||||
|
||||
private enum StyleIndex
|
||||
{
|
||||
SimpleTextWithoutBorder = 0,
|
||||
SimpleTextWithBorder = 1,
|
||||
BoldTextWithoutBorder = 2,
|
||||
BoldTextWithBorder = 3
|
||||
}
|
||||
|
||||
private void CreateCell(int columnIndex, uint rowIndex, string text, StyleIndex styleIndex)
|
||||
{
|
||||
var columnName = GetExcelColumnName(columnIndex);
|
||||
var cellReference = columnName + rowIndex;
|
||||
var row = _sheetData.Elements<Row>().FirstOrDefault(r => r.RowIndex! == rowIndex);
|
||||
if (row == null)
|
||||
{
|
||||
row = new Row() { RowIndex = rowIndex };
|
||||
_sheetData.Append(row);
|
||||
}
|
||||
|
||||
var newCell = row.Elements<Cell>()
|
||||
.FirstOrDefault(c => c.CellReference != null && c.CellReference.Value == columnName + rowIndex);
|
||||
if (newCell == null)
|
||||
{
|
||||
Cell? refCell = null;
|
||||
foreach (Cell cell in row.Elements<Cell>())
|
||||
{
|
||||
if (cell.CellReference?.Value != null && cell.CellReference.Value.Length == cellReference.Length)
|
||||
{
|
||||
if (string.Compare(cell.CellReference.Value, cellReference, true) > 0)
|
||||
{
|
||||
refCell = cell;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
newCell = new Cell() { CellReference = cellReference };
|
||||
row.InsertBefore(newCell, refCell);
|
||||
}
|
||||
|
||||
newCell.CellValue = new CellValue(text);
|
||||
newCell.DataType = CellValues.String;
|
||||
newCell.StyleIndex = (uint)styleIndex;
|
||||
}
|
||||
|
||||
private static string GetExcelColumnName(int columnNumber)
|
||||
{
|
||||
columnNumber += 1;
|
||||
int dividend = columnNumber;
|
||||
string columnName = string.Empty;
|
||||
int modulo;
|
||||
|
||||
while (dividend > 0)
|
||||
{
|
||||
modulo = (dividend - 1) % 26;
|
||||
columnName = Convert.ToChar(65 + modulo).ToString() + columnName;
|
||||
dividend = (dividend - modulo) / 26;
|
||||
}
|
||||
|
||||
return columnName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
public class OpenXmlWordBuilder : BaseWordBuilder
|
||||
{
|
||||
private readonly Document _document;
|
||||
|
||||
private readonly Body _body;
|
||||
|
||||
public OpenXmlWordBuilder()
|
||||
{
|
||||
_document = new Document();
|
||||
_body = _document.AppendChild(new Body());
|
||||
}
|
||||
|
||||
public override BaseWordBuilder AddHeader(string header)
|
||||
{
|
||||
var paragraph = _body.AppendChild(new Paragraph());
|
||||
var run = paragraph.AppendChild(new Run());
|
||||
run.AppendChild(new RunProperties(new Bold()));
|
||||
run.AppendChild(new Text(header));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BaseWordBuilder AddParagraph(string text)
|
||||
{
|
||||
var paragraph = _body.AppendChild(new Paragraph());
|
||||
var run = paragraph.AppendChild(new Run());
|
||||
run.AppendChild(new Text(text));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override BaseWordBuilder AddTable(int[] widths, List<string[]> data)
|
||||
{
|
||||
if (widths == null || widths.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(widths));
|
||||
}
|
||||
|
||||
if (data == null || data.Count == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
if (data.Any(x => x.Length != widths.Length))
|
||||
{
|
||||
throw new InvalidOperationException("widths.Length != data.Length");
|
||||
}
|
||||
|
||||
var table = new Table();
|
||||
table.AppendChild(new TableProperties(
|
||||
new TableBorders(
|
||||
new TopBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
|
||||
new BottomBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
|
||||
new LeftBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
|
||||
new RightBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
|
||||
new InsideHorizontalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
|
||||
new InsideVerticalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 }
|
||||
)
|
||||
));
|
||||
|
||||
// Заголовок
|
||||
var tr = new TableRow();
|
||||
for (var j = 0; j < widths.Length; ++j)
|
||||
{
|
||||
tr.Append(new TableCell(
|
||||
new TableCellProperties(new TableCellWidth() { Width = widths[j].ToString() }),
|
||||
new Paragraph(new Run(new RunProperties(new Bold()), new Text(data.First()[j])))));
|
||||
}
|
||||
table.Append(tr);
|
||||
|
||||
// Данные
|
||||
table.Append(data.Skip(1).Select(x =>
|
||||
new TableRow(x.Select(y => new TableCell(new Paragraph(new Run(new Text(y))))))));
|
||||
|
||||
_body.Append(table);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override Stream Build()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using var wordDocument = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document);
|
||||
var mainPart = wordDocument.AddMainDocumentPart();
|
||||
mainPart.Document = _document;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,13 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||
<PackageReference Include="PDFsharp-MigraDoc" Version="6.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TwoFromTheCasketContracts\TwoFromTheCasketContracts.csproj" />
|
||||
<InternalsVisibleTo Include="TwoFromTheCasketTests" />
|
||||
<InternalsVisibleTo Include="TwoFromTheCasketWebApi" />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
|
||||
namespace TwoFromTheCasketContracts.AdapterContracts;
|
||||
|
||||
public interface IReportAdapter
|
||||
{
|
||||
Task<ReportOperationResponse> GetRoomHistoryGroupedAsync(CancellationToken ct);
|
||||
Task<ReportOperationResponse> CreateDocumentRoomHistoryAsync(CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
using TwoFromTheCasketContracts.Infastructure;
|
||||
using TwoFromTheCasketContracts.ViewModels;
|
||||
|
||||
namespace TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
|
||||
|
||||
public class ReportOperationResponse : OperationResponse
|
||||
{
|
||||
public static ReportOperationResponse OK(List<RoomWithHistoryViewModel> data) => OK<ReportOperationResponse, List<RoomWithHistoryViewModel>>(data);
|
||||
public static ReportOperationResponse OK(Stream data, string filename) => OK<ReportOperationResponse, Stream>(data, filename);
|
||||
|
||||
public static ReportOperationResponse BadRequest(string message) => BadRequest<ReportOperationResponse>(message);
|
||||
|
||||
public static ReportOperationResponse InternalServerError(string message) => InternalServerError<ReportOperationResponse>(message);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
|
||||
namespace TwoFromTheCasketContracts.BusinessLogicsContracts;
|
||||
|
||||
public interface IReportContract
|
||||
{
|
||||
Task<List<RoomWithHistoryDataModel>> GetRoomHistoryGroupedAsync(CancellationToken ct);
|
||||
Task<Stream> CreateDocumentRoomHistoryAsync(CancellationToken ct);
|
||||
}
|
||||
@@ -5,12 +5,21 @@ using TwoFromTheCasketContracts.Infastructure;
|
||||
|
||||
namespace TwoFromTheCasketContracts.DataModels;
|
||||
|
||||
public class RoomHistoryDataModel(string roomId, string ownerFIO, TypeRoom type, DateTime dateChange) : IValidation
|
||||
public class RoomHistoryDataModel : IValidation
|
||||
{
|
||||
public string RoomId { get; private set; } = roomId;
|
||||
public string OwnerFIO { get; private set; } = ownerFIO;
|
||||
public TypeRoom Type { get; private set; } = type;
|
||||
public DateTime DateChange { get; private set; } = dateChange;
|
||||
public string RoomId { get; private set; }
|
||||
public string OwnerFIO { get; private set; }
|
||||
public TypeRoom Type { get; private set; }
|
||||
public DateTime DateChange { get; private set; }
|
||||
public RoomHistoryDataModel() { }
|
||||
|
||||
public RoomHistoryDataModel(string roomId, string ownerFIO, TypeRoom type, DateTime dateChange)
|
||||
{
|
||||
RoomId = roomId;
|
||||
OwnerFIO = ownerFIO;
|
||||
Type = type;
|
||||
DateChange = dateChange;
|
||||
}
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TwoFromTheCasketContracts.DataModels;
|
||||
|
||||
public class RoomWithHistoryDataModel
|
||||
{
|
||||
public required RoomDataModel Room { get; set; }
|
||||
public required List<RoomHistoryDataModel> History { get; set; }
|
||||
}
|
||||
@@ -7,8 +7,9 @@ namespace TwoFromTheCasketContracts.Infastructure;
|
||||
public class OperationResponse
|
||||
{
|
||||
protected HttpStatusCode StatusCode { get; set; }
|
||||
|
||||
protected object? Result { get; set; }
|
||||
protected string? FileName { get; set; }
|
||||
|
||||
public IActionResult GetResponse(HttpRequest request, HttpResponse
|
||||
response)
|
||||
{
|
||||
@@ -19,8 +20,17 @@ public class OperationResponse
|
||||
{
|
||||
return new StatusCodeResult((int)StatusCode);
|
||||
}
|
||||
if (Result is Stream stream)
|
||||
{
|
||||
return new FileStreamResult(stream, "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = FileName
|
||||
};
|
||||
}
|
||||
return new ObjectResult(Result);
|
||||
}
|
||||
protected static TResult OK<TResult, TData>(TData data, string fileName) where TResult :
|
||||
OperationResponse, new() => new() { StatusCode = HttpStatusCode.OK, Result = data, FileName = fileName };
|
||||
protected static TResult OK<TResult, TData>(TData data) where TResult :
|
||||
OperationResponse, new() => new() { StatusCode = HttpStatusCode.OK, Result = data };
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace TwoFromTheCasketContracts.StorageContracts;
|
||||
public interface IRoomHistoryStorageContract
|
||||
{
|
||||
List<RoomHistoryDataModel> GetList(string roomId);
|
||||
Task<List<RoomHistoryDataModel>> GetListAsync(CancellationToken ct);
|
||||
RoomHistoryDataModel? GetLatestByRoomId(string roomId);
|
||||
void AddElement(RoomHistoryDataModel roomHistoryDataModel);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace TwoFromTheCasketContracts.StorageContracts;
|
||||
public interface IRoomStorageContract
|
||||
{
|
||||
List<RoomDataModel> GetList();
|
||||
Task<List<RoomDataModel>> GetListAsync(CancellationToken ct);
|
||||
List<RoomHistoryDataModel> GetHistoryRoomId(string id);
|
||||
List<RoomDataModel> GetListByOwner(string OwnerFIO);
|
||||
List<RoomDataModel> GetListByAddress(string Address);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
|
||||
namespace TwoFromTheCasketContracts.ViewModels;
|
||||
|
||||
public class RoomWithHistoryViewModel
|
||||
{
|
||||
public required RoomDataModel Room { get; set; }
|
||||
public required List<RoomHistoryDataModel> History { get; set; }
|
||||
}
|
||||
@@ -18,7 +18,12 @@ public class RoomHistoryStorageContract : IRoomHistoryStorageContract
|
||||
var conf = new MapperConfiguration(cfg =>
|
||||
{
|
||||
cfg.CreateMap<RoomHistoryDataModel, RoomHistory>();
|
||||
cfg.CreateMap<RoomHistory, RoomHistoryDataModel>();
|
||||
cfg.CreateMap<RoomHistory, RoomHistoryDataModel>()
|
||||
.ForMember(dest => dest.RoomId, opt => opt.MapFrom(src => src.RoomId))
|
||||
.ForMember(dest => dest.OwnerFIO, opt => opt.MapFrom(src => src.OwnerFIO))
|
||||
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type))
|
||||
.ForMember(dest => dest.DateChange, opt => opt.MapFrom(src => src.ChangeDate));
|
||||
|
||||
});
|
||||
_mapper = new Mapper(conf);
|
||||
}
|
||||
@@ -39,6 +44,27 @@ public class RoomHistoryStorageContract : IRoomHistoryStorageContract
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RoomHistoryDataModel>> GetListAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await _dbContext.RoomHistories
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(x => x.ChangeDate)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return entities
|
||||
.Select(x => _mapper.Map<RoomHistoryDataModel>(x))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dbContext.ChangeTracker.Clear();
|
||||
throw new StorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public RoomHistoryDataModel? GetLatestByRoomId(string roomId)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -38,6 +38,25 @@ public class RoomStorageContract : IRoomStorageContract
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RoomDataModel>> GetListAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await _dbContext.Rooms
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
return entities
|
||||
.Select(x => _mapper.Map<RoomDataModel>(x))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dbContext.ChangeTracker.Clear();
|
||||
throw new StorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public List<RoomHistoryDataModel> GetHistoryRoomId(string id)
|
||||
{
|
||||
try
|
||||
@@ -160,4 +179,6 @@ public class RoomStorageContract : IRoomStorageContract
|
||||
throw new StorageException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TwoFromTheCasketBusinessLogic.Implementation;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
using TwoFromTheCasketContracts.Exceptions;
|
||||
using TwoFromTheCasketContracts.StorageContracts;
|
||||
using TwoFromTheCasketContracts.Enums;
|
||||
using TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
|
||||
namespace TwoFromTheCasketTests.BusinessLogicsContractsTests;
|
||||
|
||||
[TestFixture]
|
||||
public class ReportContractTests
|
||||
{
|
||||
private ReportContract _reportContract;
|
||||
private Mock<IRoomStorageContract> _roomStorageMock;
|
||||
private Mock<IRoomHistoryStorageContract> _roomHistoryStorageMock;
|
||||
private Mock<BaseWordBuilder> _baseWordBuilderMock;
|
||||
private Mock<BaseExcelBuilder> _baseExcelBuilderMock;
|
||||
private Mock<BasePdfBuilder> _basePdfBuilderMock;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_roomStorageMock = new Mock<IRoomStorageContract>();
|
||||
_roomHistoryStorageMock = new Mock<IRoomHistoryStorageContract>();
|
||||
_baseWordBuilderMock = new Mock<BaseWordBuilder>();
|
||||
_baseExcelBuilderMock = new Mock<BaseExcelBuilder>();
|
||||
_basePdfBuilderMock = new Mock<BasePdfBuilder>();
|
||||
|
||||
_reportContract = new ReportContract(
|
||||
_roomStorageMock.Object,
|
||||
_roomHistoryStorageMock.Object,
|
||||
new Mock<ILogger>().Object,
|
||||
_baseWordBuilderMock.Object,
|
||||
_baseExcelBuilderMock.Object,
|
||||
_basePdfBuilderMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRoomHistoryGroupedAsync_ReturnsCorrectGrouping_Test()
|
||||
{
|
||||
// Arrange
|
||||
var room1 = new RoomDataModel(Guid.NewGuid().ToString(), "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
|
||||
var room2 = new RoomDataModel(Guid.NewGuid().ToString(), "Петров", "ул. Гагарина, д. 2", 30, TypeRoom.Storage);
|
||||
var rooms = new List<RoomDataModel> { room1, room2 };
|
||||
|
||||
var history = new List<RoomHistoryDataModel>
|
||||
{
|
||||
new(room1.Id, "Иванов", TypeRoom.Office, DateTime.UtcNow.AddDays(-3)),
|
||||
new(room1.Id, "Иванов И.И.", TypeRoom.Office, DateTime.UtcNow.AddDays(-1)),
|
||||
new(room2.Id, "Петров", TypeRoom.Storage, DateTime.UtcNow.AddDays(-2)),
|
||||
};
|
||||
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
|
||||
|
||||
// Act
|
||||
var result = await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(2));
|
||||
Assert.That(result.First(x => x.Room.Id == room1.Id).History.Count, Is.EqualTo(2));
|
||||
Assert.That(result.First(x => x.Room.Id == room2.Id).History.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRoomHistoryGroupedAsync_RoomListIsNull_ThrowsInvalidOperationException()
|
||||
{
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync((List<RoomDataModel>)null);
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
|
||||
|
||||
Assert.That(async () => await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None),
|
||||
Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRoomHistoryGroupedAsync_StorageThrows_ThrowsStorageException()
|
||||
{
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new StorageException(new InvalidOperationException()));
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
|
||||
|
||||
Assert.That(async () => await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None),
|
||||
Throws.TypeOf<StorageException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateDocumentRoomHistoryAsync_ShouldReturnStream_WhenDataIsValid()
|
||||
{
|
||||
// Arrange
|
||||
var room1 = new RoomDataModel(Guid.NewGuid().ToString(), "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
|
||||
var room2 = new RoomDataModel(Guid.NewGuid().ToString(), "Петров", "ул. Гагарина, д. 2", 30, TypeRoom.Storage);
|
||||
var rooms = new List<RoomDataModel> { room1, room2 };
|
||||
|
||||
var history = new List<RoomHistoryDataModel>
|
||||
{
|
||||
new(room1.Id, "Иванов", TypeRoom.Office, DateTime.UtcNow.AddDays(-3)),
|
||||
new(room1.Id, "Иванов И.И.", TypeRoom.Office, DateTime.UtcNow.AddDays(-1)),
|
||||
new(room2.Id, "Петров", TypeRoom.Storage, DateTime.UtcNow.AddDays(-2)),
|
||||
};
|
||||
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
|
||||
|
||||
var expectedStream = new MemoryStream();
|
||||
_baseWordBuilderMock.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.AddParagraph(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>())).Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.Build()).Returns(expectedStream);
|
||||
|
||||
// Act
|
||||
var result = await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.EqualTo(expectedStream));
|
||||
|
||||
_baseWordBuilderMock.Verify(x => x.AddHeader("История изменений помещений"), Times.Once);
|
||||
_baseWordBuilderMock.Verify(x => x.AddParagraph(It.Is<string>(s => s.StartsWith("Сформировано:"))), Times.Once);
|
||||
_baseWordBuilderMock.Verify(x => x.AddTable(new[] { 20, 20, 30, 15, 20 }, It.IsAny<List<string[]>>()), Times.Once);
|
||||
_baseWordBuilderMock.Verify(x => x.Build(), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateDocumentRoomHistoryAsync_ShouldThrowInvalidOperationException_WhenNoDataFound()
|
||||
{
|
||||
// Arrange
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel>());
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(async () => await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None),
|
||||
Throws.TypeOf<NullReferenceException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateDocumentRoomHistoryAsync_ShouldThrowStorageException_WhenStorageFails()
|
||||
{
|
||||
// Arrange
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new StorageException(new InvalidOperationException()));
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(async () => await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None),
|
||||
Throws.TypeOf<StorageException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateDocumentRoomHistoryAsync_ShouldCreateCorrectTableStructure()
|
||||
{
|
||||
// Arrange
|
||||
var room = new RoomDataModel("room1", "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
|
||||
var history = new List<RoomHistoryDataModel>
|
||||
{
|
||||
new(room.Id, "Иванов", TypeRoom.Office, DateTime.Parse("2023-01-01")),
|
||||
new(room.Id, "Иванов И.И.", TypeRoom.Office, DateTime.Parse("2023-01-02")),
|
||||
};
|
||||
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel> { room });
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
|
||||
|
||||
List<string[]> actualTableData = null;
|
||||
_baseWordBuilderMock.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.AddParagraph(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>()))
|
||||
.Callback<int[], List<string[]>>((_, data) => actualTableData = data)
|
||||
.Returns(_baseWordBuilderMock.Object);
|
||||
_baseWordBuilderMock.Setup(x => x.Build()).Returns(new MemoryStream());
|
||||
|
||||
// Act
|
||||
await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.That(actualTableData, Is.Not.Null);
|
||||
Assert.That(actualTableData.Count, Is.EqualTo(4));
|
||||
|
||||
// Verify header
|
||||
Assert.That(actualTableData[0], Is.EqualTo(new[] { "RoomId", "OwnerFIO", "Address", "Type", "DateChanged" }));
|
||||
|
||||
// Verify room row
|
||||
Assert.That(actualTableData[1][0], Is.EqualTo("room1"));
|
||||
Assert.That(actualTableData[1][1], Is.EqualTo("Иванов"));
|
||||
Assert.That(actualTableData[1][2], Is.EqualTo("ул. Ленина, д. 1"));
|
||||
Assert.That(actualTableData[1][3], Is.EqualTo("Office"));
|
||||
Assert.That(actualTableData[1][4], Is.EqualTo(""));
|
||||
|
||||
// Verify history rows
|
||||
Assert.That(actualTableData[2][0], Is.EqualTo(""));
|
||||
Assert.That(actualTableData[2][1], Is.EqualTo("Иванов"));
|
||||
Assert.That(actualTableData[2][2], Is.EqualTo(""));
|
||||
Assert.That(actualTableData[2][3], Is.EqualTo("Office"));
|
||||
Assert.That(actualTableData[2][4], Is.EqualTo("2023-01-01 00:00"));
|
||||
|
||||
Assert.That(actualTableData[3][0], Is.EqualTo(""));
|
||||
Assert.That(actualTableData[3][1], Is.EqualTo("Иванов И.И."));
|
||||
Assert.That(actualTableData[3][2], Is.EqualTo(""));
|
||||
Assert.That(actualTableData[3][3], Is.EqualTo("Office"));
|
||||
Assert.That(actualTableData[3][4], Is.EqualTo("2023-01-02 00:00"));
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,52 @@ namespace TwoFromTheCasketTests.BusinessLogicsContractsTests
|
||||
_roomStorageMock.Verify(x => x.GetList(), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllRoomsAsync_ReturnsListOfRooms()
|
||||
{
|
||||
// Arrange
|
||||
var rooms = new List<RoomDataModel>
|
||||
{
|
||||
new(Guid.NewGuid().ToString(), "John Doe", "ул. Ленина, д. 10", 50, TypeRoom.Office),
|
||||
new(Guid.NewGuid().ToString(), "Jane Doe", "ул. Гагарина, д. 20", 75, TypeRoom.PublicBuilding),
|
||||
new(Guid.NewGuid().ToString(), "Alice Smith", "ул. Пушкина, д. 5", 100, TypeRoom.Residential)
|
||||
};
|
||||
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
|
||||
|
||||
// Act
|
||||
var result = await _roomStorageMock.Object.GetListAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Has.Count.EqualTo(3));
|
||||
Assert.That(result, Is.EquivalentTo(rooms));
|
||||
});
|
||||
|
||||
_roomStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllRoomsAsync_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel>());
|
||||
|
||||
// Act
|
||||
var result = await _roomStorageMock.Object.GetListAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Has.Count.EqualTo(0));
|
||||
});
|
||||
|
||||
_roomStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRoomsByOwner_ReturnsListOfRooms()
|
||||
{
|
||||
|
||||
@@ -110,6 +110,48 @@ public class RoomHistoryBusinessLogicContractTests
|
||||
_roomHistoryStorageMock.Verify(x => x.GetList(roomId), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRoomHistoryAsync_ReturnsListOfRecords_Test()
|
||||
{
|
||||
var roomId = Guid.NewGuid().ToString();
|
||||
var listOriginal = new List<RoomHistoryDataModel>
|
||||
{
|
||||
new(roomId, Guid.NewGuid().ToString(), TwoFromTheCasketContracts.Enums.TypeRoom.Office, DateTime.UtcNow.AddMonths(-2)),
|
||||
new(roomId, Guid.NewGuid().ToString(), TwoFromTheCasketContracts.Enums.TypeRoom.PublicBuilding, DateTime.UtcNow.AddMonths(-1))
|
||||
};
|
||||
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(listOriginal);
|
||||
|
||||
var result = await _roomHistoryStorageMock.Object.GetListAsync(CancellationToken.None);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.EquivalentTo(listOriginal));
|
||||
});
|
||||
|
||||
_roomHistoryStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRoomHistoryAsync_ReturnsEmptyList_Test()
|
||||
{
|
||||
var roomId = Guid.NewGuid().ToString();
|
||||
|
||||
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<RoomHistoryDataModel>());
|
||||
|
||||
var result = await _roomHistoryStorageMock.Object.GetListAsync(CancellationToken.None);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(0));
|
||||
});
|
||||
|
||||
_roomHistoryStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetLatestRoomHistory_ReturnsLatestRecord_Test()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using TwoFromTheCasketContracts.AdapterContracts;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
using TwoFromTheCasketContracts.Enums;
|
||||
using TwoFromTheCasketContracts.Exceptions;
|
||||
using TwoFromTheCasketTests.Infrastructure;
|
||||
using TwoFromTheCasketTests.WebApiControllersTests;
|
||||
|
||||
namespace TwoFromTheCasketTests.ControllerTests;
|
||||
|
||||
public class ReportControllerTests : BaseWebApiControllerTest
|
||||
{
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
TwoFromTheCasketDb.ClearTables();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRoomHistoryGrouped_WhenHaveRecords_ShouldSuccess_Test()
|
||||
{
|
||||
var room = TwoFromTheCasketDb.InsertRoomToDatabaseAndReturn(address: "ул. Ленина, д. 1", owner: "Иванов Иван", space: 45, type: TypeRoom.Office);
|
||||
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Петров Петр", type: TypeRoom.PublicBuilding, date: DateTime.UtcNow.AddMonths(-1));
|
||||
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Сидоров Сидор", type: TypeRoom.Residential, date: DateTime.UtcNow.AddMonths(-2));
|
||||
|
||||
var response = await HttpClient.GetAsync("/api/report/GetRoomHistory");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var data = await GetModelFromResponseAsync<List<RoomWithHistoryDataModel>>(response);
|
||||
Assert.That(data, Is.Not.Null);
|
||||
Assert.That(data, Has.Count.EqualTo(1));
|
||||
Assert.That(data[0].Room.Id, Is.EqualTo(room.Id));
|
||||
Assert.That(data[0].History, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LoadRoomHistory_ShouldReturnFileStreamResult_WhenDataIsValid()
|
||||
{
|
||||
// Arrange
|
||||
var room = TwoFromTheCasketDb.InsertRoomToDatabaseAndReturn(address: "ул. Ленина, д. 1", owner: "Иванов Иван", space: 45, type: TypeRoom.Office);
|
||||
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Петров Петр", type: TypeRoom.PublicBuilding, date: DateTime.UtcNow.AddMonths(-1));
|
||||
|
||||
// Act
|
||||
var response = await HttpClient.GetAsync("/api/report/LoadRoomHistory");
|
||||
|
||||
// Assert
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/octet-stream"));
|
||||
Assert.That(response.Content.Headers.ContentDisposition?.FileName, Is.EqualTo("history.docx"));
|
||||
Assert.That(await response.Content.ReadAsStreamAsync(), Is.Not.Null);
|
||||
}
|
||||
|
||||
private static async Task<T> GetModelFromResponseAsync<T>(HttpResponseMessage response)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TwoFromTheCasketBusinessLogic.Implementation;
|
||||
using TwoFromTheCasketContracts.AdapterContracts;
|
||||
using TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
|
||||
using TwoFromTheCasketContracts.BusinessLogicsContracts;
|
||||
using TwoFromTheCasketContracts.DataModels;
|
||||
using TwoFromTheCasketContracts.Exceptions;
|
||||
using TwoFromTheCasketContracts.ViewModels;
|
||||
|
||||
namespace TwoFromTheCasketWebApi.Adapters;
|
||||
|
||||
public class ReportAdapter : IReportAdapter
|
||||
{
|
||||
private readonly IReportContract _reportLogic;
|
||||
private readonly ILogger<ReportAdapter> _logger;
|
||||
private readonly Mapper _mapper;
|
||||
|
||||
public ReportAdapter(IReportContract reportLogic, ILogger<ReportAdapter> logger)
|
||||
{
|
||||
_reportLogic = reportLogic;
|
||||
_logger = logger;
|
||||
|
||||
var config = new MapperConfiguration(cfg =>
|
||||
{
|
||||
cfg.CreateMap<RoomDataModel, RoomViewModel>();
|
||||
cfg.CreateMap<RoomHistoryDataModel, RoomHistoryViewModel>();
|
||||
cfg.CreateMap<RoomWithHistoryDataModel, RoomWithHistoryViewModel>();
|
||||
});
|
||||
|
||||
_mapper = new Mapper(config);
|
||||
}
|
||||
|
||||
public async Task<ReportOperationResponse> CreateDocumentRoomHistoryAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ReportOperationResponse.OK(await _reportLogic.CreateDocumentRoomHistoryAsync(ct), "history.docx");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "InvalidOperationException");
|
||||
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException!.Message}");
|
||||
}
|
||||
catch (StorageException ex)
|
||||
{
|
||||
_logger.LogError(ex, "StorageException");
|
||||
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException!.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception");
|
||||
return
|
||||
ReportOperationResponse.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ReportOperationResponse> GetRoomHistoryGroupedAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _reportLogic.GetRoomHistoryGroupedAsync(ct);
|
||||
var mapped = result.Select(x => _mapper.Map<RoomWithHistoryViewModel>(x)).ToList();
|
||||
return ReportOperationResponse.OK(mapped);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "InvalidOperationException");
|
||||
return ReportOperationResponse.BadRequest("Data is not initialized");
|
||||
}
|
||||
catch (StorageException ex)
|
||||
{
|
||||
_logger.LogError(ex, "StorageException");
|
||||
return ReportOperationResponse.InternalServerError($"Storage error: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception");
|
||||
return ReportOperationResponse.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TwoFromTheCasketContracts.AdapterContracts;
|
||||
using TwoFromTheCasketWebApi.Adapters;
|
||||
|
||||
namespace TwoFromTheCasketWebApi.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class ReportController(IReportAdapter reportAdapter) : ControllerBase
|
||||
{
|
||||
private IReportAdapter _reportAdapter = reportAdapter;
|
||||
|
||||
[HttpGet]
|
||||
[Consumes("application/json")]
|
||||
public async Task<IActionResult> GetRoomHistory(CancellationToken ct)
|
||||
{
|
||||
return (await _reportAdapter.GetRoomHistoryGroupedAsync(ct)).GetResponse(Request, Response);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Consumes("application/octet-stream")]
|
||||
public async Task<IActionResult> LoadRoomHistory(CancellationToken ct)
|
||||
{
|
||||
return (await _reportAdapter.CreateDocumentRoomHistoryAsync(ct)).GetResponse(Request, Response);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using Serilog;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using TwoFromTheCasketBusinessLogic.Implementation;
|
||||
using TwoFromTheCasketBusinessLogic.OfficePackage;
|
||||
using TwoFromTheCasketContracts.AdapterContracts;
|
||||
using TwoFromTheCasketContracts.BusinessLogicsContracts;
|
||||
using TwoFromTheCasketContracts.Infastructure;
|
||||
@@ -82,6 +83,12 @@ public class Program
|
||||
builder.Services.AddTransient<IWorkerAdapter, WorkerAdapter>();
|
||||
builder.Services.AddTransient<ISalaryAdapter, SalaryAdapter>();
|
||||
|
||||
builder.Services.AddTransient<IReportContract, ReportContract>();
|
||||
builder.Services.AddTransient<IReportAdapter, ReportAdapter>();
|
||||
builder.Services.AddTransient<BaseExcelBuilder, OpenXmlExcelBuilder>();
|
||||
builder.Services.AddTransient<BaseWordBuilder, OpenXmlWordBuilder>();
|
||||
builder.Services.AddTransient<BasePdfBuilder, MigraDocPdfBuilder>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
Reference in New Issue
Block a user