я умер

This commit is contained in:
Milana Ievlewa 2024-05-31 23:22:37 +04:00
parent 1cda9ec164
commit 5348aa00c1
13 changed files with 250 additions and 292 deletions

View File

@ -11,6 +11,7 @@ using BeautySalonContracts.BindingModels;
using BeautySalonContracts.SearchModels;
using BeatySalonBusinesLogic.OfficePackage.HelperModels;
using BeatySalonBusinesLogic.OfficePackage.Implements;
using BeautySalonBusinesLogics.OfficePackage;
namespace BeautySalonBusinesLogic.BusinessLogics
{
@ -60,16 +61,16 @@ namespace BeautySalonBusinesLogic.BusinessLogics
public List<ReportServicesViewModel> GetServices(ReportServiceBindingModel model)
{
List<ReportServicesViewModel> r = _serviceStorage.GetFullList()
.Select(x => new ReportServicesViewModel
{
Id = x.Id,
ProcedureName = x.ServiceProcedures.Select(kv => kv.Value.Item1.ProcedureName).FirstOrDefault(),
CosmeticName = x.ServiceCosmetics.Select(kv => kv.Value.Item1.CosmeticName).FirstOrDefault(),
ServicePrice = x.ServiceProcedures.Sum(kv => kv.Value.Item1.ProcedurePrice) +
x.ServiceCosmetics.Sum(kv => kv.Value.Item1.CosmeticPrice)
}).ToList();
return r;
List<ReportServicesViewModel> r = _serviceStorage.GetFullList()
.Select(x => new ReportServicesViewModel
{
Id = x.Id,
Cosmetics = x.ServiceCosmetics,
Procedures = x.ServiceProcedures,
ServicePrice = x.ServiceProcedures.Sum(kv => kv.Value.Item1.ProcedurePrice) +
x.ServiceCosmetics.Sum(kv => kv.Value.Item1.CosmeticPrice)
}).ToList();
return r;
}
public void SaveCosmeticProceduresToWordFile(ReportBindingModel model)
@ -93,7 +94,7 @@ namespace BeautySalonBusinesLogic.BusinessLogics
public void SaveServicesToPdfFile(ReportServiceBindingModel model)
{
_saveToPdf.CreateDoc(new PdfInfo
_saveToPdf.CreateReport(new PdfInfo
{
FileName = model.FileName,
Title = "Список услуг",

View File

@ -1,68 +1,123 @@
using BeatySalonBusinesLogic.OfficePackage.HelperEnums;
using BeatySalonBusinesLogic.OfficePackage.HelperModels;
using BeautySalonContracts.ViewModels;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BeatySalonBusinesLogic.OfficePackage
namespace BeautySalonBusinesLogics.OfficePackage
{
public abstract class AbstractSaveToPdfStorekeeper//services
/// <summary>
/// Абстрактный класс для создания отчета Pdf
/// </summary>
public abstract class AbstractSaveToPdfStorekeeper
{
public void CreateDoc(PdfInfo info)
public void CreateReport(PdfInfo info)
{
// Создаем файл
CreatePdf(info);
// Создаем заголовок
CreateParagraph(new PdfParagraph
{
Text = info.Title,
Style = "NormalTitle",
ParagraphAlignment = PdfParagraphAlignmentType.Center
Style = "NormalTitle"
});
// Период выборки данных
// "с XX.XX.XXXX по XX.XX.XXXX"
CreateParagraph(new PdfParagraph
{
Text = $"с{info.DateFrom.ToShortDateString()} по {info.DateTo.ToShortDateString()}",
Style = "Normal",
ParagraphAlignment = PdfParagraphAlignmentType.Center
Text = $"С {info.DateFrom.ToShortDateString()} по {info.DateTo.ToShortDateString()}",
Style = "Normal"
});
CreateTable(new List<string> { "2cm", "6cm", "3cm"});
// Создаем таблицу с тремя колонками
CreateTable(new List<string> { "7cm", "4cm", "4cm" });
// Создаем заголовок таблицы
CreateRow(new PdfRowParameters
{
Texts = new List<string> { "Номер", "Процедура", "Косметика", "Сумма"},
Texts = new List<string> { "Услуга", "Процедуры", "Косметика" },
Style = "NormalTitle",
ParagraphAlignment = PdfParagraphAlignmentType.Center
});
foreach (var service in info.Services)
// Записываем основную информацию
foreach (var view in info.Services)
{
CreateRow(new PdfRowParameters
{
Texts = new List<string> { service.Id.ToString(), service.ProcedureName, service.CosmeticName, service.ServicePrice.ToString()},
Texts = new List<string> { view.ServiceName, "", "" },
Style = "Normal",
ParagraphAlignment = PdfParagraphAlignmentType.Left
});
// Конвертируем из HashSet в List, чтобы можно было обращаться по индексу
List<string> cosmetics = new List<string>();
foreach (var item in view.Cosmetics)
{
var cosmeticModel = item.Value.Item1;
cosmetics.Add(cosmeticModel.CosmeticName);
}
List<string> procedures = new List<string>();
foreach (var item in view.Procedures)
{
var procedureModel = item.Value.Item1;
procedures.Add(procedureModel.ProcedureName);
}
// Записываем названия лекарств во 2 колонку
// и названия лекарств в 3 колонку
int maxLength = Math.Max(cosmetics.Count, procedures.Count);
for (int i = 0; i < maxLength; i++)
{
string cosmetic = (i < cosmetics.Count) ? cosmetics[i] : "";
string procedure = (i < procedures.Count) ? procedures[i] : "";
CreateRow(new PdfRowParameters
{
Texts = new List<string> { "", cosmetic, procedure },
Style = "Normal",
ParagraphAlignment = PdfParagraphAlignmentType.Left
});
}
}
CreateParagraph(new PdfParagraph
{
Text = $"Итого: {info.Services.Sum(x => x.ServicePrice)}\t",
Style = "Normal",
ParagraphAlignment = PdfParagraphAlignmentType.Right
});
// Сохраняем файл
SavePdf(info);
}
//Создание файла
/// <summary>
/// Создать файл Pdf
/// </summary>
/// <param name="info"></param>
protected abstract void CreatePdf(PdfInfo info);
//Создание параграфа
/// <summary>
/// Создать абзац с текстом
/// </summary>
/// <param name="paragraph"></param>
protected abstract void CreateParagraph(PdfParagraph paragraph);
//Создание таблицы
/// <summary>
/// Создать таблицу
/// </summary>
/// <param name="columns"></param>
protected abstract void CreateTable(List<string> columns);
//Создание и заполнение строки
/// <summary>
/// Создать и заполнить строку
/// </summary>
/// <param name="rowParameters"></param>
protected abstract void CreateRow(PdfRowParameters rowParameters);
//Сохранение файла
/// <summary>
/// Сохранить файл Pdf
/// </summary>
/// <param name="info"></param>
protected abstract void SavePdf(PdfInfo info);
}
}

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BeautySalonBusinesLogics.OfficePackage;
namespace BeatySalonBusinesLogic.OfficePackage.Implements
{

View File

@ -1,4 +1,5 @@
using BeautySalonContracts.ViewModels;
using BeautySalonDataModels.Models;
using System;
using System.Collections.Generic;
using System.Linq;
@ -11,8 +12,8 @@ namespace BeautySalonContracts.ViewModels
{
public int Id { get; set; }
public string ServiceName { get; set; } = string.Empty;
public string ProcedureName { get; set; } = string.Empty;
public string CosmeticName { get; set; } = string.Empty;
public Dictionary <int, (ICosmeticModel, int)> Cosmetics { get; set; } = new();
public Dictionary <int, (IProcedureModel, int)> Procedures { get; set; } = new();
public double ServicePrice { get; set; }
}
}

View File

@ -10,6 +10,7 @@ using System.Reflection.PortableExecutable;
using CafeBusinessLogic.BusinessLogics;
using BeautySalonBusinesLogic.BusinessLogics;
using BeautySalonDatabaseImplement.Implements;
using BeautySalonBusinesLogics.OfficePackage;
var builder = WebApplication.CreateBuilder(args);

View File

@ -150,8 +150,6 @@ namespace StorekeeperWebApp.Controllers
APIStorekeeper.Storekeeper = null;
Response.Redirect("Enter");
}
[HttpGet]
public IActionResult Reports()
{
if (APIStorekeeper.Storekeeper == null)
@ -188,85 +186,68 @@ namespace StorekeeperWebApp.Controllers
}
[HttpPost]
public void CreateReportWord(List<int> procedures, DateTime dateFrom, DateTime dateTo)
public async Task<IActionResult> CreateReportWord(List<int> cosmetics, DateTime dateFrom, DateTime dateTo, [FromServices] IWebHostEnvironment hostingEnvironment)
{
if (APIStorekeeper.Storekeeper == null)
{
throw new Exception("Необходимо авторизоваться!");
}
if (dateFrom == DateTime.MinValue || dateTo == DateTime.MinValue)
{
throw new Exception("Введены не все данные!");
}
// Проверки ввода и авторизации
if (procedures == null || procedures.Count <= 0)
{
throw new Exception("Не выбраны рецепты!");
}
var fileName = $"Список процедур {DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.docx";
var filePath = Path.Combine(hostingEnvironment.ContentRootPath, "Downloads", fileName);
_reportLogic.SaveCosmeticProceduresToWordFile(new ReportBindingModel
{
FileName = $@"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Downloads\Список процедур {DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.docx",
FileName = filePath,
DateFrom = dateFrom,
DateTo = dateTo
});
Response.Redirect("/Home/Reports");
// Возвращаем файл для загрузки
var fileBytes = await System.IO.File.ReadAllBytesAsync(filePath);
return File(fileBytes, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", fileName);
}
[HttpPost]
public void CreateReportExcel(List<int> procedures, DateTime dateFrom, DateTime dateTo)
public async Task<IActionResult> CreateReportExcel(List<int> cosmetics, DateTime dateFrom, DateTime dateTo, [FromServices] IWebHostEnvironment hostingEnvironment)
{
if (APIStorekeeper.Storekeeper == null)
{
throw new Exception("Необходимо авторизоваться!");
}
if (dateFrom == DateTime.MinValue || dateTo == DateTime.MinValue)
{
throw new Exception("Введены не все данные!");
}
// Проверки ввода и авторизации
if (procedures == null || procedures.Count <= 0)
{
throw new Exception("Не выбраны рецепты!");
}
var fileName = $"Список процедур {DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.xlsx";
var filePath = Path.Combine(hostingEnvironment.ContentRootPath, "Downloads", fileName);
_reportLogic.SaveCosmeticProceduresToExcelFile(new ReportBindingModel
{
FileName = $@"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Downloads\Список процедур {DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.xlsx",
FileName = filePath,
DateFrom = dateFrom,
DateTo = dateTo
});
Response.Redirect("/Home/Reports");
// Возвращаем файл для загрузки
var fileBytes = await System.IO.File.ReadAllBytesAsync(filePath);
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
}
[HttpPost]
public void CreateReportPdf(DateTime dateFrom, DateTime dateTo)
public async Task<IActionResult> CreateReportPdf(DateTime dateFrom, DateTime dateTo, [FromServices] IWebHostEnvironment hostingEnvironment)
{
if (APIStorekeeper.Storekeeper == null)
{
throw new Exception("Необходимо авторизоваться!");
}
// Проверки авторизации и ввода
if (dateFrom == DateTime.MinValue || dateTo == DateTime.MinValue)
{
throw new Exception("Введены не все данные!");
}
var fileName = $"Список услуг {DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.pdf";
var filePath = Path.Combine(hostingEnvironment.ContentRootPath, "Downloads", fileName);
_reportLogic.SaveServicesToPdfFile(new ReportServiceBindingModel
{
FileName = $@"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Downloads\Список услуг{DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss")}.xlsx",
FileName = filePath,
DateFrom = dateFrom,
DateTo = dateTo,
StorekeeperId = APIStorekeeper.Storekeeper.Id
});
Response.Redirect("/Home/Reports");
// Возвращаем файл для загрузки
var fileBytes = await System.IO.File.ReadAllBytesAsync(filePath);
return File(fileBytes, "application/pdf", fileName);
}
[HttpPost]
public void SendReport(IFormFile fileUpload)
public async Task<IActionResult> SendReport(IFormFile fileUpload, [FromServices] IWebHostEnvironment hostingEnvironment)
{
if (APIStorekeeper.Storekeeper == null)
{
@ -279,10 +260,15 @@ namespace StorekeeperWebApp.Controllers
}
// Путь до файла
var uploadPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\Downloads\";
var uploadPath = Path.Combine(hostingEnvironment.ContentRootPath, "Downloads");
var fileName = Path.GetFileName(fileUpload.FileName);
var fullPath = Path.Combine(uploadPath, fileName);
using (var fileStream = new FileStream(fullPath, FileMode.Create))
{
await fileUpload.CopyToAsync(fileStream);
}
_mailLogic.MailSendAsync(new MailSendInfoBindingModel
{
MailAddress = APIStorekeeper.Storekeeper.Email,
@ -291,7 +277,7 @@ namespace StorekeeperWebApp.Controllers
Path = fullPath
});
Response.Redirect("/Home/Reports");
return RedirectToAction("Reports", "Home");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

View File

@ -10,6 +10,7 @@ using System.Reflection.PortableExecutable;
using CafeBusinessLogic.BusinessLogics;
using BeautySalonBusinesLogic.BusinessLogics;
using BeautySalonDatabaseImplement.Implements;
using BeautySalonBusinesLogics.OfficePackage;
var builder = WebApplication.CreateBuilder(args);

View File

@ -25,11 +25,8 @@
<None Include="Views\Cosmetic\UpdateCosmetics.cshtml" />
<None Include="Views\Home\Enter.cshtml" />
<None Include="Views\Home\Index.cshtml" />
<None Include="Views\Home\LaborCosts.cshtml" />
<None Include="Views\Home\ListCosmetics.cshtml" />
<None Include="Views\Home\Register.cshtml" />
<None Include="Views\Home\Report.cshtml" />
<None Include="Views\Home\Service.cshtml" />
<None Include="Views\Home\Reports.cshtml" />
<None Include="Views\Shared\Error.cshtml" />
<None Include="Views\Shared\UpdateLaborCosts.cshtml" />
<None Include="Views\Shared\_Layout.cshtml" />

View File

@ -1,51 +0,0 @@
@using BeautySalonContracts.ViewModels
@{
ViewData["Title"] = "Трудозатраты";
}
<h4 class="fw-bold">Трудозатраты</h4>
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div class="d-flex mb-2 gap-1">
<a asp-controller="LaborCosts" asp-action="Create" class="button-primary">
Создать
</a>
<a id="update-button" class="button-primary">
Обновить
</a>
<button id="delete-button" class="button-primary me-5">
Удалить
</button>
</div>
<div class="d-flex mb-2 gap-1">
<div class="input-group" style="width: auto;">
<input id="page-input" type="number" min="1" value="@ViewBag.Page" max="@ViewBag.NumberOfPages" class="form-control" style="max-width: 5em">
<span class="input-group-text">/ @ViewBag.NumberOfPages</span>
</div>
<a href="/Home/Cars?page=@ViewBag.Page" id="go-button" class="button-primary">
Перейти
</a>
</div>
</div>
<div class="border">
<table class="table mb-0">
<thead>
<tr>
<th>Количество часов</th>
<th>Количество специалистов</th>
</tr>
</thead>
<tbody>
@foreach (var item in ViewBag.LaborCosts)
{
<tr class="table-row" id="row-@item.Id">
<td>@item.NumberHours</td>
<td>@item.NumberSpecialists</td>
</tr>
}
</tbody>
</table>
</div>
<script src="~/js/laborcosts.js" asp-append-version="true"></script>

View File

@ -1,52 +0,0 @@
@{
ViewData["Title"] = "Список косметики по процедурам";
}
<h4 class="fw-bold">Список косметики по процедурам</h4>
@{
if (ViewBag.IsAllowed == false)
{
<h3 class="display-4">Авторизируйтесь</h3>
return;
}
<div class="mb-2">
<button id="word-button" class="button-primary">
Word
</button>
<button id="excel-button" class="button-primary">
Excel
</button>
</div>
<p class="mb-0">Выбранные процедуры:</p>
<div class="table-shell border">
<table class="table mb-0">
<thead class="table-head">
<tr>
<th>Наименование</th>
<th>Стоимость</th>
<th></th>
</tr>
</thead>
<tbody id="tbody">
<tr>
<td>Не выбрано</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">Добавить процедуру:</p>
<div class="d-flex gap-1 mb-2">
<select class="form-select mb-0" id="procedure-select"></select>
<button id="add-button" class="button-primary">
Добавить
</button>
</div>
<script src="~/js/list.js" asp-append-version="true"></script>
}

View File

@ -1,48 +0,0 @@
@{
ViewData["Title"] = "Report";
}
<h4 class="fw-bold">Отчет по услугам</h4>
<div class="d-flex flex-wrap gap-1 align-items-end mb-2">
<div class="mb-2">
<p class="mb-0">Дата начала:</p>
<input id="date-from-input" class="form-control" type="date" />
</div>
<div class="mb-2">
<p class="mb-0">Дата конца:</p>
<input id="date-to-input" class="form-control" type="date" />
</div>
<button id="generate-button" class="button-primary mb-2">
Показать
</button>
<button id="send-by-mail-button" class="button-primary mb-2">
На почту
</button>
</div>
<p class="mb-0">
<span>За период с&nbsp;</span>
<span id="date-from-span" class="fw-bold">...</span>
<span>&nbsp;по&nbsp;</span>
<span id="date-to-span" class="fw-bold">...</span>
</p>
<div class="table-shell mb-2 border">
<table class="table mb-0">
<thead class="table-head">
<tr>
<th>Дата создания</th>
<th>Услуга</th>
<th>Стоимость</th>
<th>Бренд косметики</th>
<th>Наименование</th>
<th>Стоимость косметики</th>
<th>Процедуры</th>
</tr>
</thead>
<tbody id="tbody">
</tbody>
</table>
</div>
<script src="~/js/report.js" asp-append-version="true"></script>

View File

@ -0,0 +1,121 @@
@using BeautySalonContracts.ViewModels
@model List<ReportServicesViewModel>
@{
ViewBag.Title = "Отчеты";
}
<div class="text-center">
<h2 class="display-4">Отчеты</h2>
</div>
<form method="post" enctype="multipart/form-data" style="margin-top: 50px">
<!-- Сохранить отчеты в формате Word и Excel -->
<div class="d-flex justify-content-between">
<!-- Кнопка для сохранения отчета в формате Word -->
<div class="text-center">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("CreateReportWord", "Home")">Список процедур по косметике Word</button>
</div>
<div class="row">
<div class="col-4">Процедуры по косметике:</div>
<div class="col-8">
<select name="recipes" id="recipes" class="form-control" size="4" multiple>
@foreach (var cosmetic in ViewBag.Cosmetics)
{
<option value="@cosmetic.Id">@cosmetic.Id - @cosmetic.CosmeticName</option>
}
</select>
</div>
</div>
<!-- Кнопка для сохранения отчета в формате Excel -->
<div class="text-center">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("CreateReportExcel", "Home")">Список процедур по косметике Excel</button>
</div>
</div>
<!-- Временной период выборки данных -->
<div class="d-flex justify-content-center" style="margin: 50px 0px 30px 0px">
<div class="text-center">
<label for="dateFrom">С</label>
<input type="date" id="dateFrom" name="dateFrom" class="form-control d-inline-block w-auto">
</div>
<div class="text-center">
<label for="dateTo">по</label>
<input type="date" id="dateTo" name="dateTo" class="form-control d-inline-block w-auto">
</div>
</div>
<!-- Действия для отчета в формате Pdf -->
<div class="d-flex justify-content-between">
<!-- Сохранить отчет в формате Pdf -->
<div class="text-center">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("CreateReportPdf", "Home")">Сведения об услугах Pdf</button>
</div>
<!-- Отправить отчет на почту -->
<div class="d-flex">
<label for="fileUpload" class="d-block"></label>
<input type="file" id="fileUpload" name="fileUpload" class="form-control-file d-inline-block w-auto">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("SendReport", "Home")">Отправить отчет на почту</button>
</div>
<!-- Вывести отчет на форму -->
<div class="text-center">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("Reports", "Home")">Вывести отчет на форму</button>
</div>
</div>
</form>
<!-- Таблица для вывода отчета на форму -->
<table class="table">
<thead>
<tr>
<th>Услуга</th>
<th>Процедуры</th>
<th>Косметика</th>
</tr>
</thead>
<tbody>
@if (Model == null || Model.Count <= 0)
{
<td class="text-center" colspan="3">Нет доступных данных</td>
}
else
{
foreach (var record in Model)
{
<td>@record.ServiceName</td>
<td></td>
<td></td>
// Конвертируем из HashSet в List, чтобы можно было обращаться по индексу
var cosmetics = new List<string>();
foreach (var kvp in record.Cosmetics)
{
var cosmeticModel = kvp.Value.Item1;
cosmetics.Add(cosmeticModel.CosmeticName);
}
var procedures = new List<string>();
foreach (var kvp in record.Procedures)
{
var procedureModel = kvp.Value.Item1;
cosmetics.Add(procedureModel.ProcedureName);
}
// Записываем названия лекарств во 2 колонку
// и названия лекарств в 3 колонку
int maxLength = Math.Max(cosmetics.Count, procedures.Count);
for (int i = 0; i < maxLength; i++)
{
<tr>
<td></td>
<td>@(i < cosmetics.Count ? cosmetics[i] : "")</td>
<td>@(i < procedures.Count ? procedures[i] : "")</td>
</tr>
}
}
}
</tbody>
</table>

View File

@ -1,55 +0,0 @@
@using BeautySalonContracts.ViewModels
@{
ViewData["Title"] = "Услуга";
}
<h4 class="fw-bold">Услуги</h4>
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div class="d-flex mb-2 gap-1">
<a asp-controller="Service" asp-action="Create" class="button-primary">
Создать
</a>
<a id="update-button" class="button-primary">
Обновить
</a>
<a id="bind-button" class="button-primary">
Связать с косметикой
</a>
<button id="delete-button" class="button-primary me-5">
Удалить
</button>
</div>
<div class="d-flex mb-2 gap-1">
<div class="input-group" style="width: auto;">
<input id="page-input" type="number" min="1" value="@ViewBag.Page" max="@ViewBag.NumberOfPages" class="form-control" style="max-width: 5em">
<span class="input-group-text">/ @ViewBag.NumberOfPages</span>
</div>
<a href="/Home/Cars?page=@ViewBag.Page" id="go-button" class="button-primary">
Перейти
</a>
</div>
</div>
<div class="border">
<table class="table mb-0">
<thead>
<tr>
<th>Наименование</th>
<th>Стоимость</th>
<th>Дата создания</th>
</tr>
</thead>
<tbody>
@foreach (var item in ViewBag.Service)
{
<tr class="table-row" id="row-@item.Id">
<td>@item.ServiceName</td>
<td>@item.ServicePrice</td>
<td>@item.DateCreate</td>
</tr>
}
</tbody>
</table>
</div>
<script src="~/js/service.js" asp-append-version="true"></script>