Merge branch 'Storekeeper' into Worker_Raspaev
This commit is contained in:
commit
41f189f0ee
@ -88,7 +88,7 @@ namespace HardwareShopBusinessLogic.BusinessLogics.Storekeeper
|
||||
});
|
||||
}
|
||||
|
||||
public void SaveBuildGoodToWordFile(ReportBindingModel model, List<GoodViewModel> goods)
|
||||
public byte[] SaveBuildGoodToWordFile(ReportBindingModel model, List<GoodViewModel> goods)
|
||||
{
|
||||
_saveToWord.CreateBuildGoodReport(new WordInfo
|
||||
{
|
||||
@ -96,6 +96,15 @@ namespace HardwareShopBusinessLogic.BusinessLogics.Storekeeper
|
||||
Title = "Cписок сборок по выбранным товарам",
|
||||
BuildGood = GetBuildGood(goods)
|
||||
});
|
||||
|
||||
byte[] file = File.ReadAllBytes(model.FileName);
|
||||
File.Delete(model.FileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
byte[] IReportStorekeeperLogic.SaveBuildGoodToExcelFile(ReportBindingModel model, List<GoodViewModel> goods)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,55 +27,33 @@ namespace HardwareShopBusinessLogic.OfficePackage
|
||||
CellToName = "C1"
|
||||
});
|
||||
|
||||
/*uint rowIndex = 2;
|
||||
foreach (var ss in info.BuildGood)
|
||||
uint rowIndex = 2;
|
||||
foreach (var bg in info.BuildGood)
|
||||
{
|
||||
InsertCellInWorksheet(new ExcelCellParameters
|
||||
{
|
||||
ColumnName = "A",
|
||||
RowIndex = rowIndex,
|
||||
Text = ss.ShopName,
|
||||
Text = bg.GoodName,
|
||||
StyleInfo = ExcelStyleInfoType.Text
|
||||
});
|
||||
rowIndex++;
|
||||
|
||||
foreach (var (Sushi, Count) in ss.ListSushi)
|
||||
foreach (var build in bg.Builds)
|
||||
{
|
||||
InsertCellInWorksheet(new ExcelCellParameters
|
||||
{
|
||||
ColumnName = "B",
|
||||
RowIndex = rowIndex,
|
||||
Text = Sushi,
|
||||
StyleInfo = ExcelStyleInfoType.TextWithBroder
|
||||
});
|
||||
|
||||
InsertCellInWorksheet(new ExcelCellParameters
|
||||
{
|
||||
ColumnName = "C",
|
||||
RowIndex = rowIndex,
|
||||
Text = Count.ToString(),
|
||||
Text = build,
|
||||
StyleInfo = ExcelStyleInfoType.TextWithBroder
|
||||
});
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
InsertCellInWorksheet(new ExcelCellParameters
|
||||
{
|
||||
ColumnName = "A",
|
||||
RowIndex = rowIndex,
|
||||
Text = "Итого",
|
||||
StyleInfo = ExcelStyleInfoType.Text
|
||||
});
|
||||
InsertCellInWorksheet(new ExcelCellParameters
|
||||
{
|
||||
ColumnName = "C",
|
||||
RowIndex = rowIndex,
|
||||
Text = ss.TotalCount.ToString(),
|
||||
StyleInfo = ExcelStyleInfoType.Text
|
||||
});
|
||||
rowIndex++;
|
||||
}*/
|
||||
}
|
||||
|
||||
SaveExcel(info);
|
||||
}
|
||||
|
@ -23,23 +23,34 @@ namespace HardwareShopBusinessLogic.OfficePackage
|
||||
rows.Add(new WordRow
|
||||
{
|
||||
Rows = new List<(string, WordTextProperties)> {
|
||||
("Название", new WordTextProperties { Size = "24", Bold = true }),
|
||||
("Адрес", new WordTextProperties { Size = "24", Bold = true }),
|
||||
("Дата открытия", new WordTextProperties { Size = "24", Bold = true })
|
||||
("Товары", new WordTextProperties { Size = "24", Bold = true }),
|
||||
("Сборки", new WordTextProperties { Size = "24", Bold = true })
|
||||
}
|
||||
});
|
||||
|
||||
/*foreach (var shop in info.BuildGood)
|
||||
var reportRecords = info.BuildGood;
|
||||
foreach (var reportRecord in reportRecords)
|
||||
{
|
||||
rows.Add(new WordRow
|
||||
{
|
||||
Rows = new List<(string, WordTextProperties)> {
|
||||
(shop.ShopName, new WordTextProperties { Size = "24" }),
|
||||
(shop.Address, new WordTextProperties { Size = "24" }),
|
||||
(shop.DateOpening.ToShortDateString(), new WordTextProperties { Size = "24" })
|
||||
Rows = new List<(string, WordTextProperties)>
|
||||
{
|
||||
(reportRecord.GoodName, new WordTextProperties { Size = "24" }),
|
||||
("", new WordTextProperties { })
|
||||
}
|
||||
});
|
||||
}*/
|
||||
for (int i = 0; i < reportRecord.Builds.Count; i++)
|
||||
{
|
||||
rows.Add(new WordRow
|
||||
{
|
||||
Rows = new List<(string, WordTextProperties)>
|
||||
{
|
||||
("", new WordTextProperties { }),
|
||||
(reportRecord.Builds[i], new WordTextProperties { Size = "24" })
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CreateTable(rows);
|
||||
|
||||
|
@ -4,6 +4,7 @@ using HardwareShopDatabaseImplement.Models;
|
||||
using HardwareShopDatabaseImplement.Models.Storekeeper;
|
||||
using HardwareShopDataModels.Enums;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace HardwareShopStorekeeperApp.Controllers
|
||||
{
|
||||
@ -115,7 +116,7 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public void CreateGood([FromBody]GoodBindingModel goodModel)
|
||||
public void CreateGood([FromBody] GoodBindingModel goodModel)
|
||||
{
|
||||
if (APIClient.User == null)
|
||||
{
|
||||
@ -162,6 +163,21 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
APIClient.PostRequest("api/good/updatedata", goodModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public GoodViewModel? GetGood(int Id)
|
||||
{
|
||||
if (APIClient.User == null)
|
||||
{
|
||||
throw new Exception("Вы как сюда попали? Сюда вход только авторизованным");
|
||||
}
|
||||
if (Id <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификатор товара не может быть меньше или равен 0");
|
||||
}
|
||||
var result = APIClient.GetRequest<GoodViewModel>($"api/good/getgood?id={Id}");
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public Tuple<GoodViewModel, List<Tuple<ComponentViewModel?, int>>>? GetGoodUpdate(int goodid)
|
||||
{
|
||||
@ -183,7 +199,7 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
}
|
||||
if (good <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор товара не может быть меньше или равен 0");
|
||||
throw new Exception($"Идентификатор товара не может быть меньше или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/good/deletegood", new GoodBindingModel
|
||||
{
|
||||
@ -221,7 +237,7 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
}
|
||||
if (buildId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор сборки не может быть меньше или равен 0");
|
||||
throw new Exception($"Идентификатор сборки не может быть меньше или равен 0");
|
||||
}
|
||||
var result = APIClient.GetRequest<BuildViewModel>($"api/build/getbuild?buildId={buildId}");
|
||||
return result;
|
||||
@ -296,7 +312,7 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
}
|
||||
if (component <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор комплектующего не может быть меньше или равен 0");
|
||||
throw new Exception($"Идентификатор комплектующего не может быть меньше или равен 0");
|
||||
}
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
@ -325,7 +341,7 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
}
|
||||
if (component <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор комплектующего не может быть меньше или равен 0");
|
||||
throw new Exception($"Идентификатор комплектующего не может быть меньше или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/component/deletecomponent", new ComponentBindingModel
|
||||
{
|
||||
@ -375,9 +391,28 @@ namespace HardwareShopStorekeeperApp.Controllers
|
||||
{
|
||||
return Redirect("~/Home/Enter");
|
||||
}
|
||||
ViewBag.Goods = APIClient.GetRequest<List<GoodViewModel>>($"api/good/getgoods?userId={APIClient.User.Id}");
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public void ListBuilds([FromBody] GoodBindingModel goodModel, [FromQuery] string format, [FromQuery] string filename)
|
||||
{
|
||||
if (APIClient.User == null)
|
||||
{
|
||||
throw new Exception("Вы как сюда попали? Сюда вход только авторизованным");
|
||||
}
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
throw new FormatException($"Неправильный формат файла: {format}");
|
||||
}
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
throw new FormatException($"Неправильное название файла: {filename}");
|
||||
}
|
||||
APIClient.PostRequest($"api/report/buildgoodreport?format={format}&filename=${filename}", goodModel);
|
||||
}
|
||||
|
||||
public IActionResult Report()
|
||||
{
|
||||
if (APIClient.User == null)
|
||||
|
@ -8,21 +8,129 @@
|
||||
<h1 class="display-4">Получение списка сборок</h1>
|
||||
</div>
|
||||
|
||||
<form method="post" class="d-flex flex-column align-items-center">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<h1 class="display-6">Выбранные товары</h1>
|
||||
<div class="d-flex justify-content-center">
|
||||
<button type="button" class="btn btn-primary mx-2 mt-3" data-bs-toggle="modal" data-bs-target="#exampleModal">Добавить товар</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Товар
|
||||
</th>
|
||||
<th scope="col">Товар</th>
|
||||
<th scope="col">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="result">
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="col d-flex justify-content-evenly align-items-baseline">
|
||||
<button type="button" class="btn btn-primary btn-lg m-2">Сохранить в doc-формате</button>
|
||||
<button type="button" class="btn btn-primary btn-lg m-2">Сохранить в xls-формате</button>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label">Название файла</label>
|
||||
<input type="text" class="form-control" name="filename" id="filename">
|
||||
</div>
|
||||
</form>
|
||||
<div class="col d-flex justify-content-evenly align-items-baseline">
|
||||
<button type="button" class="btn btn-primary btn-lg m-2" id="savedoc">Сохранить в doc-формате</button>
|
||||
<button type="button" class="btn btn-primary btn-lg m-2" id="saveexcel">Сохранить в xls-формате</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Товар</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">Товар</label>
|
||||
<select id="good" name="good" class="form-control" asp-items="@(new SelectList(@ViewBag.Goods, "Id", "GoodName"))"></select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="savegood">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts
|
||||
{
|
||||
<script>
|
||||
let list = [];
|
||||
const submitGoodBtn = document.getElementById("savegood");
|
||||
const resultTable = document.getElementById("result");
|
||||
const saveDocBtn = document.getElementById("savedoc");
|
||||
const saveExcelBtn = document.getElementById("saveexcel");
|
||||
const filename = document.getElementById("filename");
|
||||
|
||||
submitGoodBtn.addEventListener("click", () => {
|
||||
console.log('try to add good')
|
||||
var good = $('#good').val();
|
||||
if (good)
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: `/Storekeeper/GetGood`,
|
||||
data: { Id: good },
|
||||
success: function (result) {
|
||||
let flag = false
|
||||
if (list.length > 0) {
|
||||
list.forEach((elem) => {
|
||||
if (elem.id === parseInt(result.id)) {
|
||||
console.log('good already added')
|
||||
flag = true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!flag) list.push(result)
|
||||
reloadTable()
|
||||
}
|
||||
}).fail(function(xhr, textStatus, errorThrown) {
|
||||
alert(xhr.responseText);
|
||||
})
|
||||
})
|
||||
|
||||
saveDocBtn.addEventListener("click", async () => {
|
||||
send('doc')
|
||||
})
|
||||
|
||||
saveExcelBtn.addEventListener("click", async () => {
|
||||
send('excel')
|
||||
})
|
||||
|
||||
function send(format) {
|
||||
console.log(`try to save in ${format} format`)
|
||||
if (list.length == 0 || !filename.value || filename.value == '') {
|
||||
alert('operation failed. goods or filename are empty')
|
||||
return
|
||||
}
|
||||
$.ajax({
|
||||
url: `/Storekeeper/ListBuilds?format=${format}&filename=${filename.value}`,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ "Goods" : list })
|
||||
}).done(() => {
|
||||
//let byteArray = new Uint8Array(file);
|
||||
})
|
||||
.fail(function(xhr, textStatus, errorThrown) {
|
||||
alert(xhr.responseText);
|
||||
})
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
resultTable.innerHTML = ''
|
||||
let count = 0;
|
||||
list.forEach((elem) => {
|
||||
resultTable.innerHTML += `<tr><td>${elem.goodName}</td><td> \
|
||||
<div> \
|
||||
<button onclick="deleteGood(${count})" type="button" class="btn btn-danger"> \
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> \
|
||||
</button> \
|
||||
</div></td></tr>`
|
||||
count++;
|
||||
})
|
||||
}
|
||||
|
||||
function deleteGood(id) {
|
||||
list = list.filter(value => value.goodName != resultTable.rows[id].cells[0].innerText)
|
||||
reloadTable()
|
||||
}
|
||||
</script>
|
||||
}
|
@ -19,16 +19,25 @@ namespace HardwareShopContracts.BindingModels
|
||||
set;
|
||||
} = new();
|
||||
|
||||
// for dictionary item1
|
||||
public List<ComponentViewModel> GoodComponentsComponents
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
// for dictionary item2
|
||||
public List<int> GoodComponentsCounts
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
// for report list
|
||||
public List<GoodViewModel> Goods
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
}
|
||||
}
|
||||
|
@ -24,12 +24,12 @@ namespace HardwareShopContracts.BusinessLogicsContracts
|
||||
/// Сохранение списка сборок по выбранным товарам в файл-Word
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
void SaveBuildGoodToWordFile(ReportBindingModel model, List<GoodViewModel> goods);
|
||||
byte[] SaveBuildGoodToWordFile(ReportBindingModel model, List<GoodViewModel> goods);
|
||||
|
||||
/// <summary>
|
||||
/// Сохранение списка сборок по выбранным товарам в файл-Excel
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
void SaveBuildGoodToExcelFile(ReportBindingModel model, List<GoodViewModel> goods);
|
||||
byte[] SaveBuildGoodToExcelFile(ReportBindingModel model, List<GoodViewModel> goods);
|
||||
}
|
||||
}
|
||||
|
BIN
HardwareShop/HardwareShopRestApi/$q.doc
Normal file
BIN
HardwareShop/HardwareShopRestApi/$q.doc
Normal file
Binary file not shown.
@ -0,0 +1,46 @@
|
||||
using HardwareShopContracts.BindingModels;
|
||||
using HardwareShopContracts.BusinessLogicsContracts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HardwareShopRestApi.Controllers
|
||||
{
|
||||
[Route("api/[controller]/[action]")]
|
||||
[ApiController]
|
||||
public class ReportController : Controller
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IReportStorekeeperLogic _reportStorekeeperLogic;
|
||||
private readonly IWorkerReportLogic _reportWorkerLogic;
|
||||
|
||||
public ReportController(ILogger<ReportController> logger, IReportStorekeeperLogic reportStorekeeperLogic, IWorkerReportLogic workerReportLogic)
|
||||
{
|
||||
_logger = logger;
|
||||
_reportStorekeeperLogic = reportStorekeeperLogic;
|
||||
_reportWorkerLogic = workerReportLogic;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public void BuildGoodReport(GoodBindingModel model, string format, string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case "doc":
|
||||
_reportStorekeeperLogic.SaveBuildGoodToWordFile(new ReportBindingModel { FileName = filename }, model.Goods);
|
||||
break;
|
||||
case "excel":
|
||||
_reportStorekeeperLogic.SaveBuildGoodToExcelFile(new ReportBindingModel { FileName = filename }, model.Goods);
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("Неправильный формат файла");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка сохранения списка сборок по выбранным товарам");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
using HardwareShopBusinessLogic.BusinessLogics;
|
||||
using HardwareShopBusinessLogic.BusinessLogics.Storekeeper;
|
||||
using HardwareShopBusinessLogic.OfficePackage;
|
||||
using HardwareShopBusinessLogic.OfficePackage.Implements;
|
||||
using HardwareShopContracts.BuisnessLogicsContracts;
|
||||
using HardwareShopContracts.BusinessLogicsContracts;
|
||||
using HardwareShopContracts.StoragesContracts;
|
||||
@ -29,6 +31,10 @@ builder.Services.AddTransient<IComponentLogic, ComponentLogic>();
|
||||
builder.Services.AddTransient<IGoodLogic, GoodLogic>();
|
||||
builder.Services.AddTransient<IOrderLogic, OrderLogic>();
|
||||
builder.Services.AddTransient<IPurchaseLogic, PurchaseLogic>();
|
||||
builder.Services.AddTransient<IReportStorekeeperLogic, ReportStorekeeperLogic>();
|
||||
builder.Services.AddTransient<IWorkerReportLogic, WorkerReportLogic>();
|
||||
builder.Services.AddTransient<AbstractSaveToWord, SaveToWord>();
|
||||
builder.Services.AddTransient<AbstractSaveToExcel, SaveToExcel>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
|
@ -115,7 +115,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (buildId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор сборки не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор сборки не может быть ниже или равен 0");
|
||||
}
|
||||
var result = APIClient.GetRequest<BuildViewModel>($"api/build/getBuild?buildId={buildId}");
|
||||
if (result == null)
|
||||
@ -138,7 +138,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (buildId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор сборки не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор сборки не может быть ниже или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/build/update", new BuildBindingModel
|
||||
{
|
||||
@ -180,7 +180,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (deleteBuildId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор сборки не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор сборки не может быть ниже или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/build/DeleteBuild", new BuildBindingModel
|
||||
{
|
||||
@ -237,7 +237,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (commentId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор комментария не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор комментария не может быть ниже или равен 0");
|
||||
}
|
||||
var result = APIClient.GetRequest<CommentViewModel>($"api/comment/getcomment?commentId={commentId}");
|
||||
if (result == null)
|
||||
@ -260,7 +260,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (commentId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор комментария не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор комментария не может быть ниже или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/comment/update", new CommentBindingModel
|
||||
{
|
||||
@ -280,7 +280,7 @@ namespace HardwareShopWorkerApp.Controllers
|
||||
}
|
||||
if (deleteCommentId <= 0)
|
||||
{
|
||||
throw new Exception($"Идентификтаор комментария не может быть ниже или равен 0");
|
||||
throw new Exception($"Идентификатор комментария не может быть ниже или равен 0");
|
||||
}
|
||||
APIClient.PostRequest("api/comment/delete", new CommentBindingModel
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user