Большое обновление
This commit is contained in:
@@ -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++;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
@page "/otchislenie/questionnaire"
|
||||
@using Otchinslator.Components.Layout
|
||||
@layout OtchislenieLayout
|
||||
@using BlazorPageScript
|
||||
|
||||
<PageScript Src="./Components/Pages/Questionnaire.razor.js" />
|
||||
|
||||
<style>
|
||||
.knobs, .layer
|
||||
{
|
||||
.knobs, .layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -13,8 +15,7 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.button-custom
|
||||
{
|
||||
.button-custom {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
width: 5rem;
|
||||
@@ -24,8 +25,7 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-custom
|
||||
{
|
||||
.checkbox-custom {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -36,13 +36,11 @@
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.knobs
|
||||
{
|
||||
.knobs {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#button-custom-1 .knobs:before
|
||||
{
|
||||
#button-custom-1 .knobs:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
@@ -53,13 +51,12 @@
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
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%;
|
||||
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: '$';
|
||||
left: 42px;
|
||||
background-color: #89E592;
|
||||
@@ -93,7 +90,10 @@
|
||||
</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="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="card rounded-badge bg-base-200 p-4">
|
||||
<h2 class="card-title text-center text-3xl justify-center my-4">Кто ты воин?</h2>
|
||||
@@ -135,53 +135,11 @@
|
||||
</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]">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
106
src/Otchinslator/Components/Pages/Questionnaire.razor.js
Normal file
106
src/Otchinslator/Components/Pages/Questionnaire.razor.js
Normal 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();
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
@layout OtchislenieLayout
|
||||
|
||||
<script>
|
||||
function showCongratulation() {
|
||||
document.getElementById('downloadPDF').addEventListener('click', function () {
|
||||
document.getElementById('congratulation').classList.remove('hidden');
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog id="SendToDirectorModal" class="modal modal-bottom sm:modal-middle">
|
||||
@@ -20,34 +21,62 @@
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col space-y-4 xl:w-1/2 sm:w-full mx-auto">
|
||||
<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>
|
||||
<button id="downloadPDF" onclick="showCongratulation()" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
@* <div class="w-full"> *@
|
||||
@* <div class="flex flex-col space-y-4 xl:w-1/2 sm:w-full mx-auto"> *@
|
||||
<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>
|
||||
<a id="downloadPDF" target="_blank" href="/getStatement"
|
||||
class="btn btn-primary bg-base-200 border-base-200 rounded-full flex-grow-0 w-[3rem]">
|
||||
@* Скачать PDF *@<div class="absolute rounded-full w-12 h-12">
|
||||
<img class="p-3" src="img/pdf.svg" alt=""/>
|
||||
</div>
|
||||
</a>
|
||||
</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 w-96 mx-auto" >
|
||||
<a href="/otchislenie/statement" class="btn w-96 h-14 btn-primary rounded-full text-2xl">
|
||||
Назад
|
||||
</a>
|
||||
<a href="/" class="btn w-96 h-14 btn-primary rounded-full text-2xl">
|
||||
Выход
|
||||
</a>
|
||||
</div>
|
||||
@* <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> *@
|
||||
@* <a href="/" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> *@
|
||||
@* Выход *@
|
||||
@* </a> *@
|
||||
@* </div> *@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
@page "/otchislenie/statement"
|
||||
@using Otchinslator.Components.Layout
|
||||
@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>
|
||||
94
src/Otchinslator/Components/Pages/Statement.razor.js
Normal file
94
src/Otchinslator/Components/Pages/Statement.razor.js
Normal 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();
|
||||
}
|
||||
@@ -23,8 +23,12 @@
|
||||
</Target> -->
|
||||
|
||||
<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.UI" Version="3.2.2" />
|
||||
<PackageReference Include="NPetrovich" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -32,4 +36,8 @@
|
||||
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="PDFCache\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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.Mvc.Authorization;
|
||||
using Microsoft.Identity.Web;
|
||||
using Microsoft.Identity.Web.UI;
|
||||
using Otchinslator.Components;
|
||||
using Otchinslator.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -14,13 +18,17 @@ builder.Services.AddControllersWithViews(options =>
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
options.Filters.Add(new AuthorizeFilter(policy));
|
||||
}).AddMicrosoftIdentityUI();
|
||||
}).AddMicrosoftIdentityUI().AddDataAnnotationsLocalization();
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddRazorPages(); //////////////
|
||||
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();
|
||||
|
||||
@@ -34,7 +42,7 @@ app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
app.MapRazorComponents<App>();
|
||||
// .AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
91
src/Otchinslator/Services/IStatementGenerator.cs
Normal file
91
src/Otchinslator/Services/IStatementGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
77
src/Otchinslator/StatementController.cs
Normal file
77
src/Otchinslator/StatementController.cs
Normal 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);
|
||||
// }
|
||||
}
|
||||
25
src/Otchinslator/UserDTO.cs
Normal file
25
src/Otchinslator/UserDTO.cs
Normal 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; }
|
||||
|
||||
}
|
||||
38
src/Otchinslator/UserData.cs
Normal file
38
src/Otchinslator/UserData.cs
Normal 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
59
src/Otchinslator/Utils.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
@@ -10,7 +20,7 @@
|
||||
"TenantId": "sfedu.ru",
|
||||
"ClientId": "ab679823-ea58-4c45-9af5-3284b1285aa2",
|
||||
"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"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
|
||||
Reference in New Issue
Block a user