Большое обновление

This commit is contained in:
2025-01-02 14:42:15 +03:00
parent 4428052d78
commit 098eeb468e
14 changed files with 633 additions and 118 deletions

View File

@@ -1,22 +0,0 @@
@page "/counter"
@using Otchinslator.Components.Layout
@layout OtchislenieLayout
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -2,10 +2,12 @@
@page "/otchislenie/questionnaire" @page "/otchislenie/questionnaire"
@using Otchinslator.Components.Layout @using Otchinslator.Components.Layout
@layout OtchislenieLayout @layout OtchislenieLayout
@using BlazorPageScript
<PageScript Src="./Components/Pages/Questionnaire.razor.js" />
<style> <style>
.knobs, .layer .knobs, .layer {
{
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@@ -13,8 +15,7 @@
left: 0; left: 0;
} }
.button-custom .button-custom {
{
position: relative; position: relative;
top: 50%; top: 50%;
width: 5rem; width: 5rem;
@@ -24,8 +25,7 @@
margin-top: 4px; margin-top: 4px;
} }
.checkbox-custom .checkbox-custom {
{
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -36,13 +36,11 @@
z-index: 3; z-index: 3;
} }
.knobs .knobs {
{
z-index: 2; z-index: 2;
} }
#button-custom-1 .knobs:before #button-custom-1 .knobs:before {
{
content: ''; content: '';
position: absolute; position: absolute;
top: 4px; top: 4px;
@@ -53,13 +51,12 @@
text-align: center; text-align: center;
font-size: 22px; font-size: 22px;
padding: 1px 12px 12px 11px; padding: 1px 12px 12px 11px;
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); background-color: var(--fallback-p, oklch(var(--p)/var(--tw-bg-opacity)));
border-radius: 50%; border-radius: 50%;
transition: 0.3s cubic-bezier(0.18, 0.89, 0.35, 1.15) all; transition: 0.3s cubic-bezier(0.18, 0.89, 0.35, 1.15) all;
} }
#button-custom-1 .checkbox-custom:checked + .knobs:before #button-custom-1 .checkbox-custom:checked + .knobs:before {
{
content: '$'; content: '$';
left: 42px; left: 42px;
background-color: #89E592; background-color: #89E592;
@@ -93,7 +90,10 @@
</dialog> </dialog>
<div class="relative"> <div class="relative">
<div class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/3 transform -translate-x-1/2 italic"><br>Небольшая анкета</div> <div
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/3 transform -translate-x-1/2 italic">
<br>Небольшая анкета
</div>
<div class="flex flex-col space-y-4 w-96"> <div class="flex flex-col space-y-4 w-96">
<div class="card rounded-badge bg-base-200 p-4"> <div class="card rounded-badge bg-base-200 p-4">
<h2 class="card-title text-center text-3xl justify-center my-4">Кто ты воин?</h2> <h2 class="card-title text-center text-3xl justify-center my-4">Кто ты воин?</h2>
@@ -135,53 +135,11 @@
</div> </div>
<div class="join w-full mt-4 flex gap-2"> <div class="join w-full mt-4 flex gap-2">
<button onclick="exit_modal.showModal()" class="relative btn btn-primary rounded-full flex-grow-0 w-[3rem] h-[3rem]"> <button onclick="exit_modal.showModal()"
class="relative btn btn-primary rounded-full flex-grow-0 w-[3rem] h-[3rem]">
<img class="absolute p-3" src="img/exit.svg" alt=""/> <img class="absolute p-3" src="img/exit.svg" alt=""/>
</button> </button>
<a href="otchislenie/statement" class="btn rounded-full btn-primary flex-grow w-30">Продолжим</a> <a href="otchislenie/statement" class="btn rounded-full btn-primary flex-grow w-30">Продолжим</a>
<button onclick="info_modal.showModal()" class="btn btn-primary rounded-full flex-grow-0 w-[3rem]">?</button> <button onclick="info_modal.showModal()" class="btn btn-primary rounded-full flex-grow-0 w-[3rem]">?</button>
</div> </div>
</div> </div>
@* TODO: Баг, если вернуться обратно через Nav то скрипты не грузятся *@
<script defer>
document.getElementById('phoneNumberInput').addEventListener('input', function (e) {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 10); // Limit to 10 digits
let num = e.target.value;
if (num.length > 3 && num.length <= 6) {
e.target.value = num.substring(0,3) + ' ' + num.substring(3,6);
} else if (num.length > 6 && num.length <= 8) {
e.target.value = num.substring(0,3) + ' ' + num.substring(3,6) + ' ' + num.substring(6,8);
} else if (num.length > 8) {
e.target.value = num.substring(0,3) + ' ' + num.substring(3,6) + ' ' + num.substring(6,10);
}
});
</script>
<script>
const continueButton = document.querySelector('a[href="otchislenie/statement"]');
const options = document.querySelectorAll('input[name="options"]');
const kurs = document.querySelectorAll('input[name="kurs"]');
const phoneNumberInput = document.getElementById('phoneNumberInput');
function validateForm() {
const isOptionSelected = Array.from(options).some(option => option.checked);
const isKursSelected = Array.from(kurs).some(k => k.checked);
const isPhoneNumberValid = phoneNumberInput.value.length === 12;
// log
console.log(isOptionSelected, isKursSelected, isPhoneNumberValid);
if (isOptionSelected && isKursSelected && isPhoneNumberValid) {
continueButton.classList.remove('btn-disabled');
} else {
continueButton.classList.add('btn-disabled');
}
}
options.forEach(option => option.addEventListener('change', validateForm));
kurs.forEach(k => k.addEventListener('change', validateForm));
phoneNumberInput.addEventListener('input', validateForm);
validateForm(); // Initial validation
</script>

View File

@@ -0,0 +1,106 @@
export function onLoad() {
// Подготовка поля ввода номера телефона
document.getElementById('phoneNumberInput').addEventListener('input', function (e) {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 10); // Limit to 10 digits
let num = e.target.value;
if (num.length > 3 && num.length <= 6) {
e.target.value = num.substring(0, 3) + ' ' + num.substring(3, 6);
} else if (num.length > 6 && num.length <= 8) {
e.target.value = num.substring(0, 3) + ' ' + num.substring(3, 6) + ' ' + num.substring(6, 8);
} else if (num.length > 8) {
e.target.value = num.substring(0, 3) + ' ' + num.substring(3, 6) + ' ' + num.substring(6, 10);
}
});
const continueButton = document.querySelector('a[href="otchislenie/statement"]');
const options = document.querySelectorAll('input[name="options"]');
const kursElements = document.querySelectorAll('input[name="kurs"]');
const phoneNumberInput = document.getElementById('phoneNumberInput');
const kursElement2 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '2');
const kursElement3 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '3');
const kursElement4 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '4');
const kursElement5 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '5');
// Загрузка данных из хранилища если данные есть
function loadFromLocalStorage() {
const phoneNumber = localStorage.getItem('phoneNumber');
const paid = localStorage.getItem('paid');
const option = localStorage.getItem('option');
const kurs = localStorage.getItem('kurs');
if (phoneNumber) {
phoneNumberInput.value = phoneNumber;
}
if (paid) {
document.querySelector('.checkbox-custom').checked = paid;
}
if (option) {
const optionElement = Array.from(options).find(optionElement => optionElement.getAttribute('aria-label') === option);
if (optionElement) {
optionElement.checked = true;
}
}
if (kurs) {
const kursElement = Array.from(kursElements).find(kursElement => kursElement.getAttribute('aria-label') === kurs);
if (kursElement) {
kursElement.checked = true;
}
}
}
function validateForm() {
const isOptionSelected = Array.from(options).some(option => option.checked);
const isKursSelected = Array.from(kursElements).some(k => k.checked);
const isPhoneNumberValid = phoneNumberInput.value.length === 12;
var tupeOfEducation = (Array.from(options).find(option => option.checked).getAttribute('aria-label'));
if (tupeOfEducation === "Баклан") {
kursElement3.disabled = false;
kursElement4.disabled = false;
kursElement5.disabled = true;
if (kursElement5.checked) {
kursElement5.checked = false;
kursElement4.checked = true;
}
} else if (tupeOfEducation === "Маг") {
kursElement3.disabled = true;
kursElement4.disabled = true;
kursElement5.disabled = true;
if (kursElement5.checked || kursElement4.checked || kursElement3.checked) {
kursElement5.checked = false;
kursElement4.checked = false;
kursElement3.checked = false;
kursElement2.checked = true;
}
} else if (tupeOfEducation === "Спец") {
kursElement3.disabled = false;
kursElement4.disabled = false;
kursElement5.disabled = false;
}
if (isOptionSelected && isKursSelected && isPhoneNumberValid) {
continueButton.classList.remove('btn-disabled');
localStorage.setItem('phoneNumber', phoneNumberInput.value);
localStorage.setItem('paid', document.querySelector('.checkbox-custom').checked);
localStorage.setItem('option', tupeOfEducation);
localStorage.setItem('kurs', Array.from(kursElements).find(k => k.checked).getAttribute('aria-label'));
} else {
continueButton.classList.add('btn-disabled');
}
}
options.forEach(option => option.addEventListener('change', validateForm));
kursElements.forEach(k => k.addEventListener('change', validateForm));
phoneNumberInput.addEventListener('input', validateForm);
loadFromLocalStorage();
validateForm();
}

View File

@@ -3,9 +3,10 @@
@layout OtchislenieLayout @layout OtchislenieLayout
<script> <script>
function showCongratulation() { document.getElementById('downloadPDF').addEventListener('click', function () {
document.getElementById('congratulation').classList.remove('hidden'); document.getElementById('congratulation').classList.remove('hidden');
}
});
</script> </script>
<dialog id="SendToDirectorModal" class="modal modal-bottom sm:modal-middle"> <dialog id="SendToDirectorModal" class="modal modal-bottom sm:modal-middle">
@@ -20,34 +21,62 @@
</div> </div>
</div> </div>
</dialog> </dialog>
<div class="w-full">
<div class="flex flex-col space-y-4 xl:w-1/2 sm:w-full mx-auto"> <div class="relative">
<div class="card rounded-badge bg-base-200 p-4"> @* <div class="w-full"> *@
<div class="flex flex-col space-y-4 mt-1"> @* <div class="flex flex-col space-y-4 xl:w-1/2 sm:w-full mx-auto"> *@
<button onclick="SendToDirectorModal.showModal()" class="btn h-16 btn-primary rounded-full text-2xl relative"> <object data="/getStatement" type="application/pdf" class="w-[28rem] h-[30rem] rounded-2xl">
<p class="text-center">Не удалось отобразить заявление, попробуйте скачать <a href="/getStatement">(тык)</a></p>
</object>
<div class="join w-full mt-4 flex gap-2">
<button onclick="exit_modal.showModal()" class="relative btn btn-primary rounded-full flex-grow-0 w-[3rem] h-[3rem]">
<img class="absolute p-3" src="img/exit.svg" alt=""/>
</button>
@* <button id="gen" class="btn rounded-full btn-primary flex-grow w-30">Сгенерировать</button> *@
@* <a href="otchislenie/result" class="btn rounded-full btn-primary flex-grow w-30">Сгенерировать</a> *@
<button onclick="SendToDirectorModal.showModal()"
class="btn btn-primary rounded-full flex-grow w-30">
Отправить директору Отправить директору
</button> </button>
<button id="downloadPDF" onclick="showCongratulation()" class="btn h-16 btn-primary rounded-full text-2xl relative"> <a id="downloadPDF" target="_blank" href="/getStatement"
Скачать PDF class="btn btn-primary bg-base-200 border-base-200 rounded-full flex-grow-0 w-[3rem]">
<div class="absolute bg-base-200 rounded-full right-1 w-14 h-14"> @* Скачать PDF *@<div class="absolute rounded-full w-12 h-12">
<img class="p-3" src="img/pdf.svg" alt=""/> <img class="p-3" src="img/pdf.svg" alt=""/>
</div> </div>
</button> </a>
</div> </div>
</div>
</div>
@* TODO: Сделать адаптив *@
@* <div class="card rounded-badge bg-base-200 p-4"> *@
@* <div class="flex flex-col space-y-4 mt-1"> *@
@* <button onclick="SendToDirectorModal.showModal()" *@
@* class="btn h-16 btn-primary rounded-full text-2xl relative"> *@
@* Отправить директору *@
@* </button> *@
@* <a id="downloadPDF" target="_blank" href="/getStatement" *@
@* class="btn h-16 btn-primary rounded-full text-2xl relative"> *@
@* Скачать PDF *@
@* <div class="absolute bg-base-200 rounded-full right-1 w-14 h-14"> *@
@* <img class="p-3" src="img/pdf.svg" alt=""/> *@
@* </div> *@
@* </a> *@
@* </div> *@
@* </div> *@
@* </div> *@
@* $1$ TODO: Сделать адаптив #1# *@
@* <div class="mt-9 flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0 w-96 mx-auto" > *@ @* <div class="mt-9 flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0 w-96 mx-auto" > *@
<div class="mt-9 flex flex-col space-y-4 w-96 mx-auto" > @* <div class="mt-9 flex flex-col space-y-4 w-96 mx-auto"> *@
<a href="/otchislenie/statement" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> @* <a href="/otchislenie/statement" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> *@
Назад @* Назад *@
</a> @* </a> *@
<a href="/" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> @* <a href="/" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> *@
Выход @* Выход *@
</a> @* </a> *@
</div> @* </div> *@
<div class="w-96 mx-auto mt-6"> <div class="w-96 mx-auto mt-6">
<a id="congratulation" href="/otchislenie/congratulation" class="btn w-full h-16 btn-primary rounded-full text-2xl hidden"> <a id="congratulation" href="/otchislenie/congratulation"
class="btn w-full h-16 btn-primary rounded-full text-2xl hidden">
Страница поздравления Страница поздравления
</a> </a>
</div> </div>

View File

@@ -1,8 +1,42 @@
@page "/otchislenie/statement" @page "/otchislenie/statement"
@using Otchinslator.Components.Layout @using Otchinslator.Components.Layout
@layout OtchislenieLayout @layout OtchislenieLayout
<a href="/otchislenie/result">Statement</a> @using BlazorPageScript
@code { <PageScript Src="./Components/Pages/Statement.razor.js" />
} <dialog id="generateStatementModal" class="modal">
<div class="modal-box text-center justify-center">
<h3 class="text-lg content-center items-center font-bold select-none">Генерация</h3>
<p id="loadingAnim" class="py-4"><span class="loading w-[15rem] loading-ring sm:w-[20rem]"></span></p>
<div id="failBlock" class="hidden justify-center">
<div class="flex justify-center items-center">
<svg class="w-[15rem] h-[15rem]" fill="red" stroke="red" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<p class="text-red-500">Ошибка при генерации</p>
<p class="text-red-500">Попробуйте еще раз</p>
<div class="modal-action">
<form method="dialog">
<button class="btn">Закрыть</button>
</form>
</div>
</div>
</div>
</dialog>
<div class="relative">
<div class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/3 transform -translate-x-1/2 italic"><br>Укажите причину</div>
<div class="flex flex-col space-y-4 w-96">
<textarea class="textarea textarea-bordered w-full h-96 resize-none" placeholder="Введите причину отчисления здесь..." name="statement"></textarea>
</div>
<div class="join w-full mt-4 flex gap-2">
<button onclick="exit_modal.showModal()" class="relative btn btn-primary rounded-full flex-grow-0 w-[3rem] h-[3rem]">
<img class="absolute p-3" src="img/exit.svg" alt=""/>
</button>
<button id="gen" class="btn rounded-full btn-primary flex-grow w-30">Сгенерировать</button>
@* <a href="otchislenie/result" class="btn rounded-full btn-primary flex-grow w-30">Сгенерировать</a> *@
<button onclick="info_modal.showModal()" class="btn btn-primary rounded-full flex-grow-0 w-[3rem]">?</button>
</div>
</div>

View File

@@ -0,0 +1,94 @@
export function onLoad() {
const statementField = document.querySelector('textarea[name="statement"]');
const generateButton = document.querySelector('button[id="gen"]');
const optionMapping = {
"Баклан": 1,
"Маг": 2,
"Спец": 3
};
function loadFromLocalStorage() {
const statement = localStorage.getItem('statement');
if (statement) {
statementField.value = statement;
}
}
generateButton.addEventListener('click', async () => {
hideLoadingModal(false);
console.log('Начата генерация заявления');
generateStatementModal.showModal();
const data = {
"phone": "+7" + localStorage.getItem('phoneNumber'),
"kurs": localStorage.getItem('kurs'),
"isFreeEducation": localStorage.getItem('paid') === "false",
"isOchno": false,
"speciality": optionMapping[localStorage.getItem('option')],
"reason": localStorage.getItem('statement')
}
try {
const response = await fetch("generateStatement", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
console.log('Заявление успешно сгенерировано');
location.href='otchislenie/result';
} else {
hideLoadingModal();
console.log('Ошибка при генерации заявления');
console.log(response);
}
} catch (error) {
hideLoadingModal();
console.log('Ошибка при генерации заявления');
console.log(response);
}
});
function hideLoadingModal(hide = true) {
var loadingAnim = document.getElementById('loadingAnim');
var failBlock = document.getElementById('failBlock');
if (hide) {
loadingAnim.classList.add('hidden');
failBlock.classList.remove('hidden');
} else {
loadingAnim.classList.remove('hidden');
failBlock.classList.add('hidden');
}
}
function validateField() {
// если первая буква в lowerCase, то превращаем ее в upperCase
if (statementField.value.length > 0 && statementField.value[0] === statementField.value[0].toLowerCase()) {
statementField.value = statementField.value[0].toUpperCase() + statementField.value.slice(1);
}
// удаляем пробелы если их больше одного подряд
statementField.value = statementField.value.replace(/\s{2,}/g, ' ');
localStorage.setItem('statement', statementField.value);
if (statementField.value.length > 50) {
// удаляем пробелы в начале и в конце строки
statementField.value = statementField.value.trim();
generateButton.classList.remove('btn-disabled');
} else
generateButton.classList.add('btn-disabled');
}
statementField.addEventListener('input', validateField);
loadFromLocalStorage();
validateField();
}

View File

@@ -23,8 +23,12 @@
</Target> --> </Target> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="BlazorPageScript" Version="1.0.0" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
<PackageReference Include="Gotenberg.Sharp.API.Client" Version="2.4.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.2.2" /> <PackageReference Include="Microsoft.Identity.Web" Version="3.2.2" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="3.2.2" /> <PackageReference Include="Microsoft.Identity.Web.UI" Version="3.2.2" />
<PackageReference Include="NPetrovich" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -32,4 +36,8 @@
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" /> <_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="PDFCache\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,8 +1,12 @@
using Gotenberg.Sharp.API.Client;
using Gotenberg.Sharp.API.Client.Domain.Settings;
using Gotenberg.Sharp.API.Client.Extensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web; using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI; using Microsoft.Identity.Web.UI;
using Otchinslator.Components; using Otchinslator.Components;
using Otchinslator.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -14,13 +18,17 @@ builder.Services.AddControllersWithViews(options =>
.RequireAuthenticatedUser() .RequireAuthenticatedUser()
.Build(); .Build();
options.Filters.Add(new AuthorizeFilter(policy)); options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI(); }).AddMicrosoftIdentityUI().AddDataAnnotationsLocalization();
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.Services.AddRazorPages(); ////////////// builder.Services.AddRazorPages(); //////////////
builder.Services.AddServerSideBlazor(); /////////////// builder.Services.AddServerSideBlazor(); ///////////////
builder.Services.AddOptions<GotenbergSharpClientOptions>()
.Bind(builder.Configuration.GetSection(nameof(GotenbergSharpClient)));
builder.Services.AddGotenbergSharpClient();
builder.Services.AddScoped<IStatementGenerator, StatementGenerator>();
var app = builder.Build(); var app = builder.Build();
@@ -34,7 +42,7 @@ app.UseStaticFiles();
app.UseAntiforgery(); app.UseAntiforgery();
app.MapControllers(); app.MapControllers();
app.MapRazorComponents<App>() app.MapRazorComponents<App>();
.AddInteractiveServerRenderMode(); // .AddInteractiveServerRenderMode();
app.Run(); app.Run();

View File

@@ -0,0 +1,91 @@
using System.Globalization;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Gotenberg.Sharp.API.Client;
using Gotenberg.Sharp.API.Client.Domain.Builders;
namespace Otchinslator.Services;
public interface IStatementGenerator
{
public Task<MemoryStream> GenerateStatementAsync(UserData userData);
public Task<Stream> ConvertToPDFAsync(MemoryStream stream);
}
public class StatementGenerator(GotenbergSharpClient gotenbergSharpClient) : IStatementGenerator
{
private static readonly List<string> Bookmarks = new() { "phone", "reason", "email", "kurs", "date", "fio", "paid_or_free", "ochno_or_zaochno", "speciality" };
private const string SpecialitetText = "специалитета по специальности";
private const string BakalavriatText = "бакалавриата по направлению подготовки";
private const string MagistaturaText = "магистратуры по направлению подготовки";
private const string FreeEducationText = "обучение за счет ассигнований федерального бюджета";
private const string PaidEducationText = "на договорной (платной) основе";
public async Task<MemoryStream> GenerateStatementAsync(UserData userData)
{
byte[] textByteArray = File.ReadAllBytes(@"C:\Users\Sergey\Desktop\test.docx");
MemoryStream stream = new MemoryStream();
stream.Write(textByteArray, 0, textByteArray.Length);
using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true))
{
var bookMarks = Utils.FindBookmarks(doc.MainDocumentPart.Document);
foreach (var end in bookMarks)
{
if (!Bookmarks.Contains(end.Key)) continue;
var textElement = new Text();
switch (end.Key)
{
case "phone":
textElement.Text = userData.phone;
break;
case "email":
textElement.Text = userData.email;
break;
case "kurs":
textElement.Text = userData.kurs.ToString();
break;
case "fio":
textElement.Text = userData.fio;
break;
case "paid_or_free":
textElement.Text = userData.isFreeEducation ? FreeEducationText : PaidEducationText;
break;
case "ochno_or_zaochno":
textElement.Text = userData.isOchno ? "очной" : "очно-заочной";
break;
case "date":
textElement.Text = DateTime.UtcNow.AddHours(3).ToString(CultureInfo.GetCultureInfoByIetfLanguageTag("ru-RU").DateTimeFormat.ShortDatePattern);
break;
case "reason":
textElement.Text = userData.reason;
break;
case "speciality":
textElement.Text = userData.speciality switch
{
SpecialityType.Bakalavriat => BakalavriatText,
SpecialityType.Magistatura => MagistaturaText,
SpecialityType.Specialitet => SpecialitetText,
_ => throw new ArgumentOutOfRangeException()
} + " ничего не делания";
break;
}
// Далее данный текст добавляем в закладку
var runElement = new Run(textElement);
end.Value.InsertAfterSelf(runElement);
}
}
return stream;
}
public async Task<Stream> ConvertToPDFAsync(MemoryStream stream)
{
var builder =
new MergeOfficeBuilder().WithAssets(a =>
a.AddItem("test1.docx", stream.ToArray()));
var request = await builder.BuildAsync();
return await gotenbergSharpClient.MergeOfficeDocsAsync(request);
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NPetrovich;
using Otchinslator.Services;
namespace Otchinslator;
[ApiController]
[Authorize]
[AllowAnonymous]
public class StatementController(IStatementGenerator statementGenerator) : Controller
{
[HttpPost("/generateStatement")]
public async Task<IActionResult> GenerateStatement(UserDTO userDto)
{
var userEmail = User.Identity.Name;
var userFIO = User.Claims.FirstOrDefault(x => x.Type == "name")?.Value;
var petrovich = new Petrovich()
{
FirstName = userFIO.Split(' ')[0],
LastName = userFIO.Split(' ')[1],
MiddleName = userFIO.Split(' ').Length > 2 ? userFIO.Split(' ')[2] : "",
Gender = Gender.Male
};
userFIO = petrovich.InflectFirstNameTo(Case.Genitive) + " " + petrovich.InflectLastNameTo(Case.Genitive) + " " + petrovich.InflectMiddleNameTo(Case.Genitive);
var userData = new UserData
{
reason = userDto.reason,
email = userEmail,
phone = userDto.phone,
fio = userFIO,
kurs = userDto.kurs,
isFreeEducation = userDto.isFreeEducation,
isOchno = userDto.isOchno,
speciality = userDto.speciality switch
{
1 => SpecialityType.Bakalavriat,
2 => SpecialityType.Magistatura,
3 => SpecialityType.Specialitet,
_ => throw new ArgumentOutOfRangeException()
}
};
var statement = await statementGenerator.GenerateStatementAsync(userData);
var pdf = await statementGenerator.ConvertToPDFAsync(statement);
await using var fileStream = new FileStream("./PDFCache/" + userEmail.Split('@')[0] + ".pdf", FileMode.Create, FileAccess.Write);
await pdf.CopyToAsync(fileStream);
return Ok();
}
[HttpGet("/getStatement")]
public async Task<IActionResult> GetStatement()
{
var userEmail = User.Identity.Name;
var filePath = "./PDFCache/" + userEmail.Split('@')[0] + ".pdf";
if (!System.IO.File.Exists(filePath))
return NotFound("File not found");
var outStream = new MemoryStream();
await using var file = System.IO.File.OpenRead(filePath);
await file.CopyToAsync(outStream);
outStream.Position = 0;
return File(outStream, "application/pdf");
}
// [HttpGet("/getUserData")]
// public async Task<IActionResult> getUserdata()
// {
// var userFIO = User.Claims.FirstOrDefault(x => x.Type == "name")?.Value;
// var userdata = User.Identities.FirstOrDefault()?.Claims.Select(x => new {x.Type, x.Value});
// return Ok(userdata);
// }
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
namespace Otchinslator;
public class UserDTO
{
[Required]
[StringLength(500, MinimumLength = 50, ErrorMessage = "Причина должна быть от 50 до 500 символов")]
public string reason { get; set; }
[Required]
[Phone(ErrorMessage = "Некорректный номер телефона")]
[StringLength(15, MinimumLength = 10, ErrorMessage = "Номер телефона должен быть от 10 до 15 символов")]
public string phone { get; set; }
[Required(ErrorMessage = "Курс не указан")]
[Range(1, 5, ErrorMessage = "Курс должен быть от 1 до 5")]
public int kurs { get; set; }
[Required]
public bool isFreeEducation { get; set; }
[Required]
public bool isOchno { get; set; }
[Required]
[Range(1, 3, ErrorMessage = "Некорректный тип специальности")]
public int speciality { get; set; }
}

View File

@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
namespace Otchinslator;
public class UserData
{
public UserData(
string reason,
string email,
string phone,
string fio,
int kurs,
bool isFreeEducation,
bool isOchno,
SpecialityType speciality)
{
}
public UserData()
{
}
public string reason { get; set; }
public string email { get; set; }
public string phone { get; set; }
public string fio { get; set; }
public int kurs { get; set; }
public bool isFreeEducation { get; set; }
public bool isOchno { get; set; }
public SpecialityType speciality { get; set; }
}
public enum SpecialityType
{
Bakalavriat,
Magistatura,
Specialitet
}

59
src/Otchinslator/Utils.cs Normal file
View File

@@ -0,0 +1,59 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
namespace Otchinslator;
public static class Utils
{
// Получаем все закладки в документе
// bStartWithNoEnds - словарь, который будет содержать только начало закладок,
// чтобы потом по ним находить соответствующие им концы закладок
// documentPart - элемент Word-документа
// outs - конечный результат
public static Dictionary<string, BookmarkEnd> FindBookmarks(OpenXmlElement documentPart,
Dictionary<string, BookmarkEnd> outs = null, Dictionary<string, string> bStartWithNoEnds = null)
{
if (outs == null)
{
outs = new Dictionary<string, BookmarkEnd>();
}
if (bStartWithNoEnds == null)
{
bStartWithNoEnds = new Dictionary<string, string>();
}
// Проходимся по всем элементам на странице Word-документа
foreach (var docElement in documentPart.Elements())
{
// BookmarkStart определяет начало закладки в рамках документа
// маркер начала связан с маркером конца закладки
if (docElement is BookmarkStart)
{
var bookmarkStart = docElement as BookmarkStart;
// Записываем id и имя закладки
bStartWithNoEnds.Add(bookmarkStart.Id, bookmarkStart.Name);
}
// BookmarkEnd определяет конец закладки в рамках документа
if (docElement is BookmarkEnd)
{
var bookmarkEnd = docElement as BookmarkEnd;
foreach (var startName in bStartWithNoEnds)
{
// startName.Key как раз и содержит id закладки
// здесь проверяем, что есть связь между началом и концом закладки
if (bookmarkEnd.Id == startName.Key)
// В конечный массив добавляем то, что нам и нужно получить
outs.Add(startName.Value, bookmarkEnd);
}
}
// Рекурсивно вызываем данный метод, чтобы пройтись по всем элементам
// word-документа
FindBookmarks(docElement, outs, bStartWithNoEnds);
}
return outs;
}
}

View File

@@ -1,4 +1,14 @@
{ {
"GotenbergSharpClient": {
"ServiceUrl": "http://localhost:3000",
"HealthCheckUrl": "http://localhost:3000/health",
"RetryPolicy": {
"Enabled": true,
"RetryCount": 4,
"BackoffPower": 1.5,
"LoggingEnabled": true
}
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
@@ -10,7 +20,7 @@
"TenantId": "sfedu.ru", "TenantId": "sfedu.ru",
"ClientId": "ab679823-ea58-4c45-9af5-3284b1285aa2", "ClientId": "ab679823-ea58-4c45-9af5-3284b1285aa2",
"ClientSecret": "DGu8Q~S5R8YbrNpklfB1aI5~JKdoSxEcgavKDc_s", "ClientSecret": "DGu8Q~S5R8YbrNpklfB1aI5~JKdoSxEcgavKDc_s",
"Domain": "sfedu.onmicrosoft.com", //Application ID URI without Application (client) ID and https "Domain": "sfedu.onmicrosoft.com",
"CallbackPath": "/signin-oidc" "CallbackPath": "/signin-oidc"
}, },
"AllowedHosts": "*" "AllowedHosts": "*"