init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["TrainingPlanner.Web.styles.css"]" />
|
||||
<link rel="icon" type="image/svg+xml" href="@Assets["favicon.svg"]" />
|
||||
<ImportMap />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["planner.js"]">
|
||||
</script>
|
||||
<script src="@Assets["week-drag.js"]"></script>
|
||||
<script src="@Assets["workout-drag.js"]"></script>
|
||||
<script src="@Assets["library-drag.js"]"></script>
|
||||
<script src="@Assets["exercise-picker.js"]"></script>
|
||||
<script src="@Assets["exercise-import.js"]"></script>
|
||||
<script src="@Assets["_framework/blazor.web.js"]">
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<article class="block-card">
|
||||
<header class="block-header">
|
||||
<div class="actions">
|
||||
<span class="drag-handle" draggable="true" @ondragstart="() => OnDrag.InvokeAsync()" title="Перетащить блок">⠿</span>
|
||||
<span class="block-number">@Number.ToString("00")</span>
|
||||
<strong>Упражнение</strong>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button title="Вверх" aria-label="Переместить вверх" disabled="@IsFirst" @onclick="() => OnMove.InvokeAsync(-1)">↑</button>
|
||||
<button title="Вниз" aria-label="Переместить вниз" disabled="@IsLast" @onclick="() => OnMove.InvokeAsync(1)">↓</button>
|
||||
<button @onclick="() => OnCopy.InvokeAsync()">Копия</button>
|
||||
<button class="danger-text" @onclick="() => OnDelete.InvokeAsync()">Удалить</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="exercise-columns"><ExerciseEditor Exercise="Block.Exercises[0]" OnChanged="OnChanged" /></div>
|
||||
<footer class="block-footer">
|
||||
<div class="actions">
|
||||
<label class="rest-input">Подходов
|
||||
<input type="number" min="1" step="1" @bind="Block.SetsCount" @bind:after="Changed" />
|
||||
</label>
|
||||
<label class="rest-input">Отдых, мин
|
||||
<input type="number" min="0" step="any" @bind="Block.RestMinutes" @bind:after="Changed" />
|
||||
</label>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkoutBlock Block { get; set; } = null!;
|
||||
[Parameter] public int Number { get; set; }
|
||||
[Parameter] public bool IsFirst { get; set; }
|
||||
[Parameter] public bool IsLast { get; set; }
|
||||
[Parameter] public EventCallback OnChanged { get; set; }
|
||||
[Parameter] public EventCallback<int> OnMove { get; set; }
|
||||
[Parameter] public EventCallback OnCopy { get; set; }
|
||||
[Parameter] public EventCallback OnDelete { get; set; }
|
||||
[Parameter] public EventCallback OnDrag { get; set; }
|
||||
private Task Changed() => OnChanged.InvokeAsync();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="exercise-editor">
|
||||
<div class="exercise-heading">
|
||||
<label class="grow">
|
||||
Упражнение
|
||||
<input @bind="Exercise.Name" @bind:event="oninput" @bind:after="Changed" placeholder="Название упражнения" />
|
||||
</label>
|
||||
<label class="camera-toggle" title="Нужно снять видео">
|
||||
<input type="checkbox" @bind="Exercise.RequiresVideo" @bind:after="Changed" aria-label="Нужно снять видео" />
|
||||
<span>📷</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="video-input">
|
||||
Ссылка на видео
|
||||
<input type="url" @bind="Exercise.VideoUrl" @bind:event="oninput" @bind:after="Changed"
|
||||
placeholder="https://disk.yandex.ru/…" />
|
||||
</label>
|
||||
<div class="exercise-fields">
|
||||
<label>Повторения
|
||||
<input @bind="Exercise.Repetitions" @bind:event="oninput" @bind:after="Changed" placeholder="10 или 10 (1 RIR)" />
|
||||
</label>
|
||||
<label>Вес, кг
|
||||
<input @bind="Exercise.Weight" @bind:event="oninput" @bind:after="Changed" placeholder="60 или Подобрать" />
|
||||
</label>
|
||||
<label>От максимума, %
|
||||
<input type="number" min="0" max="100" step="any" @bind="Exercise.IntensityPercent"
|
||||
@bind:after="Changed" placeholder="—" />
|
||||
</label>
|
||||
</div>
|
||||
<label>Разминочные подходы
|
||||
<textarea rows="2" @bind="Exercise.WarmupSets" @bind:event="oninput" @bind:after="Changed"
|
||||
placeholder="1 — гриф; 2 — 40 кг"></textarea>
|
||||
</label>
|
||||
<label>Примечание
|
||||
<textarea rows="2" @bind="Exercise.Notes" @bind:event="oninput" @bind:after="Changed"
|
||||
placeholder="Техника, темп или другие указания"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkoutExercise Exercise { get; set; } = null!;
|
||||
[Parameter] public EventCallback OnChanged { get; set; }
|
||||
|
||||
private Task Changed() => OnChanged.InvokeAsync();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="exercise-picker grow" @ref="root">
|
||||
@if (Groups.Count > 0)
|
||||
{
|
||||
<label>Группа упражнений
|
||||
<select aria-label="Группа упражнений" @bind="groupId" @bind:after="SearchChangedAsync">
|
||||
<option value="-1">Все группы</option>
|
||||
<option value="0">Без группы</option>
|
||||
@foreach (var group in Groups)
|
||||
{
|
||||
<option value="@group.Id">@group.Name</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
<label for="@Id">Из библиотеки</label>
|
||||
<div class="exercise-combobox">
|
||||
<input id="@Id" type="search" role="combobox" aria-label="Поиск упражнения"
|
||||
aria-autocomplete="list" aria-haspopup="listbox" aria-expanded="false"
|
||||
aria-controls="@(Id + "-results")" aria-activedescendant="@ActiveOptionId"
|
||||
@bind="search" @bind:event="oninput" @bind:after="SearchChangedAsync"
|
||||
@onfocus="OpenAsync" @onclick="OpenAsync" @onkeydown="HandleKeyAsync" disabled="@applying"
|
||||
placeholder="Выберите или найдите упражнение…" autocomplete="off" />
|
||||
<button type="button" class="picker-toggle" aria-label="Раскрыть список упражнений"
|
||||
popovertarget="@(Id + "-results")" disabled="@applying">⌄</button>
|
||||
</div>
|
||||
<div id="@(Id + "-results")" class="exercise-dropdown" popover="auto" role="listbox" aria-label="Упражнения из библиотеки">
|
||||
@for (var index = 0; index < Matches.Count; index++)
|
||||
{
|
||||
var exercise = Matches[index];
|
||||
var optionIndex = index;
|
||||
<button @key="exercise.Id" id="@(Id + "-option-" + exercise.Id)" type="button" role="option" tabindex="-1"
|
||||
class="exercise-option @(activeIndex == index ? "active" : "")"
|
||||
aria-selected="@(Value == exercise.Id ? "true" : "false")"
|
||||
@onmousedown:preventDefault @onclick="() => SelectAsync(exercise.Id, optionIndex)"
|
||||
@ondblclick="() => ApplyAsync(exercise.Id)">
|
||||
<strong>@exercise.Name</strong>
|
||||
<span>@(exercise.Group?.Name ?? "Без группы")</span>
|
||||
</button>
|
||||
}
|
||||
@if (Matches.Count == 0)
|
||||
{
|
||||
<p class="muted" role="status">Упражнения не найдены. Измените запрос.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="picker-hint">Двойной клик или Enter — добавить</p>
|
||||
}
|
||||
</div>
|
||||
@if (SelectedExercise != null)
|
||||
{
|
||||
<p class="picker-selection" role="status">Выбрано: @SelectedExercise.Name</p>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using TrainingPlanner.Application;
|
||||
using TrainingPlanner.Domain;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Editor;
|
||||
|
||||
public partial class ExercisePicker
|
||||
{
|
||||
[Inject] public IJSRuntime JavaScript { get; set; } = null!;
|
||||
[Parameter, EditorRequired] public string Id { get; set; } = string.Empty;
|
||||
[Parameter] public List<LibraryExercise> Exercises { get; set; } = [];
|
||||
[Parameter] public int Value { get; set; }
|
||||
[Parameter] public int ResetVersion { get; set; }
|
||||
[Parameter] public EventCallback<int> ValueChanged { get; set; }
|
||||
[Parameter] public EventCallback<int> OnApply { get; set; }
|
||||
private ElementReference root;
|
||||
private string search = string.Empty;
|
||||
private int groupId = -1;
|
||||
private int activeIndex = -1;
|
||||
private bool applying;
|
||||
private int lastResetVersion;
|
||||
private bool closeAfterReset;
|
||||
private List<LibraryExercise> Matches = [];
|
||||
private LibraryExercise? SelectedExercise => Exercises.SingleOrDefault(exercise => exercise.Id == Value);
|
||||
private string? ActiveOptionId => activeIndex >= 0 && activeIndex < Matches.Count
|
||||
? Id + "-option-" + Matches[activeIndex].Id : null;
|
||||
private List<ExerciseGroup> Groups => Exercises.Where(exercise => exercise.Group != null).Select(exercise => exercise.Group!)
|
||||
.DistinctBy(group => group.Id).OrderBy(group => group.Name).ToList();
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (lastResetVersion != ResetVersion)
|
||||
{
|
||||
lastResetVersion = ResetVersion;
|
||||
search = string.Empty;
|
||||
activeIndex = -1;
|
||||
closeAfterReset = true;
|
||||
}
|
||||
if (groupId > 0 && Groups.All(group => group.Id != groupId)) groupId = -1;
|
||||
UpdateMatches();
|
||||
}
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (closeAfterReset)
|
||||
{
|
||||
closeAfterReset = false;
|
||||
await JavaScript.InvokeVoidAsync("trainingPlanner.closeExercisePicker", root);
|
||||
}
|
||||
await JavaScript.InvokeVoidAsync("trainingPlanner.positionExercisePicker", root);
|
||||
}
|
||||
|
||||
private void UpdateMatches()
|
||||
{
|
||||
Matches = ExerciseSearch.Find(Exercises.Where(exercise => groupId < 0 ||
|
||||
(groupId == 0 ? exercise.GroupId == null : exercise.GroupId == groupId)), search);
|
||||
if (activeIndex >= Matches.Count) activeIndex = -1;
|
||||
}
|
||||
private async Task SearchChangedAsync()
|
||||
{
|
||||
activeIndex = -1;
|
||||
UpdateMatches();
|
||||
await ValueChanged.InvokeAsync(0);
|
||||
await OpenAsync();
|
||||
}
|
||||
private async Task OpenAsync() => await JavaScript.InvokeVoidAsync("trainingPlanner.openExercisePicker", root);
|
||||
private async Task SelectAsync(int id, int index)
|
||||
{
|
||||
activeIndex = index;
|
||||
await ValueChanged.InvokeAsync(id);
|
||||
}
|
||||
private async Task ApplyAsync(int id)
|
||||
{
|
||||
if (applying || Matches.All(exercise => exercise.Id != id)) return;
|
||||
applying = true;
|
||||
try
|
||||
{
|
||||
await JavaScript.InvokeVoidAsync("trainingPlanner.closeExercisePicker", root);
|
||||
await OnApply.InvokeAsync(id);
|
||||
}
|
||||
finally { applying = false; }
|
||||
}
|
||||
private async Task HandleKeyAsync(KeyboardEventArgs args)
|
||||
{
|
||||
if (applying) return;
|
||||
if (args.Key is "ArrowDown" or "ArrowUp")
|
||||
{
|
||||
await OpenAsync();
|
||||
if (Matches.Count == 0) return;
|
||||
activeIndex = args.Key == "ArrowDown" ? Math.Min(activeIndex + 1, Matches.Count - 1)
|
||||
: activeIndex < 0 ? Matches.Count - 1 : Math.Max(activeIndex - 1, 0);
|
||||
}
|
||||
else if (args.Key == "Enter" && await JavaScript.InvokeAsync<bool>("trainingPlanner.isExercisePickerOpen", root))
|
||||
{
|
||||
var index = activeIndex < 0 ? 0 : activeIndex;
|
||||
if (index < Matches.Count) await ApplyAsync(Matches[index].Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@inherits LayoutComponentBase
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark">TP</span>
|
||||
<span>Training<br/>
|
||||
<strong>Planner</strong>
|
||||
</span>
|
||||
</a>
|
||||
<div class="nav-label">РАБОЧЕЕ ПРОСТРАНСТВО</div>
|
||||
<nav>
|
||||
<NavLink href="/" Match="NavLinkMatch.All">◎ <span>Клиенты</span>
|
||||
</NavLink>
|
||||
<NavLink href="/exercises">▤ <span>Библиотека упражнений</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
<span class="muted">Кабинет тренера</span>
|
||||
<form method="post" action="/Logout">
|
||||
<AntiforgeryToken/>
|
||||
<button class="text-button">Выйти ↗</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main-content">@Body</main>
|
||||
</div>
|
||||
<div id="blazor-error-ui" data-nosnippet>Соединение прервано или произошла ошибка. <a href=".">Перезагрузить</a>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]">
|
||||
</script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Восстанавливаем соединение…
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Следующая попытка через <span id="components-seconds-to-next-attempt">
|
||||
</span> сек.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Не удалось восстановить соединение.<br />Попробуйте ещё раз или обновите страницу.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Повторить
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
Сеанс приостановлен.
|
||||
</p>
|
||||
<p class="components-resume-failed-visible">
|
||||
Не удалось продолжить сеанс.<br />Попробуйте ещё раз или обновите страницу.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
@inherits UiPageBase
|
||||
@inject IExerciseGroupRepository Repository
|
||||
@inject IJSRuntime JavaScript
|
||||
|
||||
<section class="panel compact group-panel">
|
||||
<fieldset class="editor-fieldset" disabled="@(Busy || Disabled || !RendererInfo.IsInteractive)">
|
||||
<div class="list-toolbar">
|
||||
<h2>Группы упражнений</h2>
|
||||
<button type="button" @onclick="NewGroup">+ Новая группа</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Error)) { <div class="error" role="alert">@Error</div> }
|
||||
<div class="group-tabs" aria-label="Группы упражнений">
|
||||
<button class="@(Value == -1 ? "selected" : "")" @onclick="() => ValueChanged.InvokeAsync(-1)">Все упражнения</button>
|
||||
<button class="@(Value == 0 ? "selected" : "")" data-exercise-group="0" @ondragover:preventDefault @ondrop:preventDefault
|
||||
@ondrop="() => DropExerciseAsync(0)" @onclick="() => ValueChanged.InvokeAsync(0)">Без группы</button>
|
||||
@foreach (var group in Groups)
|
||||
{
|
||||
<button @key="group.Id" class="@(Value == group.Id ? "selected" : "")"
|
||||
data-exercise-group="@group.Id" @ondragover:preventDefault @ondrop:preventDefault
|
||||
@ondrop="() => DropExerciseAsync(group.Id)"
|
||||
@onclick="() => ValueChanged.InvokeAsync(group.Id)">@group.Name</button>
|
||||
}
|
||||
</div>
|
||||
@if (SelectedGroup != null)
|
||||
{
|
||||
<div class="actions group-actions">
|
||||
<button @onclick="() => editing = new ExerciseGroup { Id = SelectedGroup.Id, Name = SelectedGroup.Name }">Переименовать группу</button>
|
||||
<button class="danger-text" disabled="@(Busy || Disabled || !RendererInfo.IsInteractive)" @onclick="DeleteAsync">Удалить группу</button>
|
||||
</div>
|
||||
}
|
||||
@if (editing != null)
|
||||
{
|
||||
<form class="group-form" @onsubmit="SaveAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<label>Название группы<input @bind="editing.Name" required /></label>
|
||||
<div class="actions">
|
||||
<button class="primary">Сохранить группу</button>
|
||||
<button type="button" @onclick="() => editing = null">Отмена</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<ExerciseGroup> Groups { get; set; } = [];
|
||||
[Parameter] public int Value { get; set; }
|
||||
[Parameter] public EventCallback<int> ValueChanged { get; set; }
|
||||
[Parameter] public EventCallback OnChanged { get; set; }
|
||||
[Parameter] public EventCallback<int> OnDropExercise { get; set; }
|
||||
[Parameter] public bool Disabled { get; set; }
|
||||
private ExerciseGroup? editing;
|
||||
private ExerciseGroup? SelectedGroup => Groups.SingleOrDefault(group => group.Id == Value);
|
||||
private void NewGroup() => editing = new();
|
||||
private Task DropExerciseAsync(int id) => Busy || Disabled ? Task.CompletedTask : OnDropExercise.InvokeAsync(id);
|
||||
private Task SaveAsync() => RunAsync(async () =>
|
||||
{
|
||||
await Repository.SaveAsync(editing!);
|
||||
var id = editing!.Id;
|
||||
editing = null;
|
||||
await OnChanged.InvokeAsync();
|
||||
await ValueChanged.InvokeAsync(id);
|
||||
});
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
var group = SelectedGroup;
|
||||
if (group == null || Busy || Disabled) return;
|
||||
if (!await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить группу «{group.Name}» вместе со всеми её упражнениями? Составленные тренировки сохранятся.")) return;
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
await Repository.DeleteAsync(group.Id);
|
||||
editing = null;
|
||||
await OnChanged.InvokeAsync();
|
||||
await ValueChanged.InvokeAsync(-1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@inherits UiPageBase
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<dialog @ref="dialog" class="exercise-import" aria-labelledby="import-title" @oncancel:preventDefault @oncancel="CloseAsync">
|
||||
<div class="list-toolbar">
|
||||
<h2 id="import-title">Импорт упражнений</h2>
|
||||
<button type="button" class="icon-button" aria-label="Закрыть импорт" disabled="@Busy" @onclick="CloseAsync">×</button>
|
||||
</div>
|
||||
<p class="muted">Скопируйте из Excel два столбца без заголовков и вставьте в первую ячейку.
|
||||
Строки добавятся автоматически. Ссылка необязательна; одинаковые упражнения создаются как отдельные записи.</p>
|
||||
@if (!string.IsNullOrEmpty(Error)) { <div class="error" role="alert">@Error</div> }
|
||||
<p class="error clipboard-error" role="alert" hidden></p>
|
||||
<fieldset disabled="@Busy">
|
||||
<label>Группа для импорта
|
||||
<select @bind="groupChoice">
|
||||
<option value="">Без группы</option>
|
||||
@foreach (var group in Groups) { <option value="@group.Id">@group.Name</option> }
|
||||
<option value="new">Создать новую группу</option>
|
||||
</select>
|
||||
</label>
|
||||
@if (groupChoice == "new")
|
||||
{
|
||||
<label>Название новой группы<input @bind="newGroupName" @bind:event="oninput" /></label>
|
||||
}
|
||||
<div class="import-table-scroll">
|
||||
<table class="import-table">
|
||||
<thead><tr><th>Наименование упражнения</th><th>Ссылка на видео</th><th><span class="muted">Удалить</span></th></tr></thead>
|
||||
<tbody>
|
||||
@for (var index = 0; index < rows.Count; index++)
|
||||
{
|
||||
var rowIndex = index;
|
||||
var row = rows[index];
|
||||
<tr @key="row">
|
||||
<td><input aria-label="@($"Название, строка {index + 1}")" data-row="@index" data-column="0" @bind="row.Name" @bind:event="oninput" /></td>
|
||||
<td><input aria-label="@($"Видео, строка {index + 1}")" data-row="@index" data-column="1" @bind="row.VideoUrl" @bind:event="oninput" /></td>
|
||||
<td><button type="button" class="icon-button danger-text" aria-label="@($"Удалить строку {index + 1}")" @onclick="() => RemoveRow(rowIndex)">×</button></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="list-toolbar">
|
||||
<button type="button" disabled="@(rows.Count >= Options.MaxRows)" @onclick="() => rows.Add(new())">Добавить строку</button>
|
||||
<span class="muted">Упражнений: @FilledCount · максимум @Options.MaxRows</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="primary" disabled="@(FilledCount == 0)" @onclick="ImportAsync">Подтвердить импорт</button>
|
||||
<button type="button" @onclick="CloseAsync">Отмена</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</dialog>
|
||||
@@ -0,0 +1,76 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using TrainingPlanner.Application;
|
||||
using TrainingPlanner.Domain;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Library;
|
||||
|
||||
public partial class ExerciseImport
|
||||
{
|
||||
[Inject] public IExerciseImporter Importer { get; set; } = null!;
|
||||
[Inject] public ExerciseImportOptions Options { get; set; } = null!;
|
||||
[Inject] public ClipboardTableParser Parser { get; set; } = null!;
|
||||
[Inject] public IJSRuntime JavaScript { get; set; } = null!;
|
||||
[Parameter] public List<ExerciseGroup> Groups { get; set; } = [];
|
||||
[Parameter] public int? InitialGroupId { get; set; }
|
||||
[Parameter] public EventCallback OnClose { get; set; }
|
||||
[Parameter] public EventCallback<int> OnImported { get; set; }
|
||||
private ElementReference dialog;
|
||||
private DotNetObjectReference<ExerciseImport>? reference;
|
||||
private List<ExerciseImportRow> rows = [new()];
|
||||
private string groupChoice = string.Empty;
|
||||
private string newGroupName = string.Empty;
|
||||
private int FilledCount => rows.Count(row => !string.IsNullOrWhiteSpace(row.Name) || !string.IsNullOrWhiteSpace(row.VideoUrl));
|
||||
|
||||
protected override void OnInitialized() => groupChoice = InitialGroupId?.ToString() ?? string.Empty;
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
reference = DotNetObjectReference.Create(this);
|
||||
await JavaScript.InvokeVoidAsync("exerciseImport.open", dialog, reference, Options.MaxClipboardCharacters);
|
||||
}
|
||||
[JSInvokable]
|
||||
public async Task PasteAsync(string text, int rowIndex, int columnIndex)
|
||||
{
|
||||
await RunAsync(() =>
|
||||
{
|
||||
var pasted = Parser.Parse(text);
|
||||
if (rowIndex < 0 || rowIndex >= rows.Count || columnIndex < 0 || columnIndex > 1)
|
||||
throw new InvalidOperationException("Выберите ячейку для вставки.");
|
||||
if (pasted.Any(row => row.Count + columnIndex > 2))
|
||||
throw new InvalidOperationException("В таблице два столбца. Вставьте название и ссылку начиная с первого столбца.");
|
||||
if (rowIndex + pasted.Count > Options.MaxRows)
|
||||
throw new InvalidOperationException($"В таблице может быть не более {Options.MaxRows} строк.");
|
||||
while (rows.Count < rowIndex + pasted.Count) rows.Add(new());
|
||||
for (var index = 0; index < pasted.Count; index++)
|
||||
{
|
||||
var cells = pasted[index];
|
||||
var row = rows[rowIndex + index];
|
||||
if (columnIndex == 0) row.Name = cells[0];
|
||||
else row.VideoUrl = cells[0];
|
||||
if (cells.Count == 2) row.VideoUrl = cells[1];
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
private void RemoveRow(int index)
|
||||
{
|
||||
rows.RemoveAt(index);
|
||||
if (rows.Count == 0) rows.Add(new());
|
||||
}
|
||||
private Task CloseAsync() => Busy ? Task.CompletedTask : OnClose.InvokeAsync();
|
||||
private Task ImportAsync() => RunAsync(async () =>
|
||||
{
|
||||
if (groupChoice == "new") PlanValidator.RequireName(newGroupName, "Название новой группы");
|
||||
var groupId = int.TryParse(groupChoice, out var id) ? id : (int?)null;
|
||||
var count = await Importer.ImportAsync(rows, groupId, groupChoice == "new" ? newGroupName : string.Empty);
|
||||
await OnImported.InvokeAsync(count);
|
||||
});
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try { await JavaScript.InvokeVoidAsync("exerciseImport.close", dialog); }
|
||||
catch (JSDisconnectedException) { }
|
||||
finally { reference?.Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
@page "/clients/{Id:int}"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@inherits UiPageBase
|
||||
@inject IClientRepository Clients
|
||||
@inject IPlanRepository Plans
|
||||
@inject PlanFactory Factory
|
||||
@inject PlanCopier Copier
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JavaScript
|
||||
<PageTitle>Программы клиента · Training Planner</PageTitle>
|
||||
<a class="breadcrumb" href="/">← Все клиенты</a>
|
||||
@if (!string.IsNullOrEmpty(Error)) { <div class="error" role="alert">@Error</div> }
|
||||
@if (client != null)
|
||||
{
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">КАРТОЧКА КЛИЕНТА</p>
|
||||
<h1>@client.Name</h1>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="count-badge">Программ: @plans.Count</span>
|
||||
<button type="button" class="icon-button danger-text" title="Удалить клиента" aria-label="Удалить клиента"
|
||||
disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="DeleteAsync">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<details class="panel compact">
|
||||
<summary>Данные клиента</summary>
|
||||
<form @onsubmit="SaveClientAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<label>Имя<input @bind="client.Name" required />
|
||||
</label>
|
||||
<label>Цель<textarea @bind="client.Goal" /></label>
|
||||
<label>Режим тренировок<textarea @bind="client.TrainingSchedule" /></label>
|
||||
<label>Состояние здоровья<textarea @bind="client.HealthStatus" /></label>
|
||||
<label>Заметки<textarea @bind="client.Notes" /></label>
|
||||
<button disabled="@Busy">Сохранить данные</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</details>
|
||||
<section class="panel compact">
|
||||
<h2>Программа на месяц</h2>
|
||||
<form @onsubmit="CreateAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<div class="inline-form">
|
||||
<label class="grow">Название программы<input @bind="newName" placeholder="Например, октябрь · набор массы" required />
|
||||
</label>
|
||||
<button class="primary" disabled="@Busy">Создать программу</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
<h2 class="section-title">История программ</h2>
|
||||
@if (plans.Count == 0) { <section class="empty-state">
|
||||
<h2>Пока нет программ</h2>
|
||||
<p class="muted">Создайте первую программу, а в следующем месяце используйте её как основу.</p>
|
||||
</section> }
|
||||
@foreach (var plan in plans)
|
||||
{
|
||||
<div class="program-row">
|
||||
<div>
|
||||
<a class="program-title" href="@($"/programs/{plan.Id}")">@plan.Name</a>
|
||||
<p class="muted">Создана @plan.CreatedAtUtc.ToLocalTime().ToString("dd.MM.yyyy")</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button disabled="@Busy" @onclick="() => BeginCopy(plan)">Создать на основе</button>
|
||||
<a class="button" href="@($"/programs/{plan.Id}")">Открыть →</a>
|
||||
<button type="button" class="icon-button danger-text" title="Удалить программу" aria-label="@($"Удалить программу «{plan.Name}»")"
|
||||
disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="() => DeletePlanAsync(plan)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (copySource != null)
|
||||
{
|
||||
<section class="panel compact">
|
||||
<h2>Копия: @copySource.Name</h2>
|
||||
<form @onsubmit="CopyAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<label>Новое название<input @bind="copyName" required />
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button class="primary" disabled="@Busy">Создать независимую копию</button>
|
||||
<button type="button" @onclick="() => copySource = null">Отмена</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@code {
|
||||
[Parameter] public int Id { get; set; }
|
||||
private Client? client;
|
||||
private List<TrainingPlan> plans = [];
|
||||
private string newName = string.Empty;
|
||||
private string copyName = string.Empty;
|
||||
private TrainingPlan? copySource;
|
||||
protected override Task OnParametersSetAsync() => RunAsync(async () =>
|
||||
{
|
||||
client = await Clients.GetAsync(Id);
|
||||
if (client == null) throw new InvalidOperationException("Клиент не найден.");
|
||||
plans = await Plans.GetForClientAsync(Id);
|
||||
});
|
||||
private Task SaveClientAsync() => RunAsync(() => Clients.SaveAsync(client!));
|
||||
private Task DeleteAsync() => RunAsync(async () =>
|
||||
{
|
||||
if (!await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить клиента «{client!.Name}» и все его программы тренировок? Это действие нельзя отменить.")) return;
|
||||
await Clients.DeleteAsync(Id);
|
||||
Navigation.NavigateTo("/");
|
||||
});
|
||||
private Task CreateAsync() => RunAsync(async () =>
|
||||
{
|
||||
var plan = Factory.Create(Id, newName);
|
||||
await Plans.SaveAsync(plan);
|
||||
Navigation.NavigateTo($"/programs/{plan.Id}");
|
||||
});
|
||||
private void BeginCopy(TrainingPlan plan) { copySource = plan; copyName = plan.Name; }
|
||||
private Task DeletePlanAsync(TrainingPlan plan) => RunAsync(async () =>
|
||||
{
|
||||
if (!await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить программу «{plan.Name}» со всеми её неделями и тренировками? Это действие нельзя отменить.")) return;
|
||||
await Plans.DeleteAsync(plan.Id);
|
||||
plans.Remove(plan);
|
||||
if (copySource?.Id == plan.Id) copySource = null;
|
||||
});
|
||||
private Task CopyAsync() => RunAsync(async () =>
|
||||
{
|
||||
var original = await Plans.GetAsync(copySource!.Id) ?? throw new InvalidOperationException("Программа не найдена.");
|
||||
var copy = Copier.Copy(original, copyName);
|
||||
await Plans.SaveAsync(copy);
|
||||
Navigation.NavigateTo($"/programs/{copy.Id}");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/Error"
|
||||
<PageTitle>Ошибка · Training Planner</PageTitle>
|
||||
<h1>Не удалось открыть страницу</h1>
|
||||
<p class="muted">Попробуйте обновить страницу. Если ошибка повторяется, вернитесь к списку клиентов.</p>
|
||||
<a class="button" href="/">К клиентам</a>
|
||||
@@ -0,0 +1,81 @@
|
||||
@page "/exercises"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@inherits UiPageBase
|
||||
<PageTitle>Библиотека упражнений · Training Planner</PageTitle>
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">БИБЛИОТЕКА</p>
|
||||
<h1>Упражнения</h1>
|
||||
<p class="muted">Название и ссылка на видео — для быстрого добавления в программу.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="count-badge">Упражнений: @items.Count</span>
|
||||
<button disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="() => importOpen = true">Импорт из таблицы</button>
|
||||
</div>
|
||||
</header>
|
||||
@if (importOpen)
|
||||
{
|
||||
<ExerciseImport Groups="groups" InitialGroupId="@(selectedGroupId > 0 ? selectedGroupId : null)"
|
||||
OnClose="() => importOpen = false" OnImported="ImportedAsync" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(importMessage)) { <p role="status">@importMessage</p> }
|
||||
@if (!string.IsNullOrEmpty(Error)) { <div class="error" role="alert">@Error</div> }
|
||||
<ExerciseGroups Groups="groups" Value="selectedGroupId" ValueChanged="SelectGroup" OnChanged="ReloadAsync"
|
||||
OnDropExercise="MoveToGroupAsync" Disabled="Busy" />
|
||||
<p class="muted">Перетащите упражнение на название группы или на «Без группы», чтобы перенести его.</p>
|
||||
<section class="panel compact">
|
||||
<h2>@(editing.Id == 0 ? "Новое упражнение" : "Редактирование упражнения")</h2>
|
||||
<form @onsubmit="SaveAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<div class="form-grid">
|
||||
<label>Название<input @bind="editing.Name" required />
|
||||
</label>
|
||||
<label>Ссылка на видео<input type="url" @bind="editing.VideoUrl" placeholder="https://disk.yandex.ru/…" />
|
||||
</label>
|
||||
</div>
|
||||
<label>Группа
|
||||
<select @bind="editing.GroupId">
|
||||
<option value="">Без группы</option>
|
||||
@foreach (var group in groups) { <option value="@group.Id">@group.Name</option> }
|
||||
</select>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button class="primary" disabled="@Busy">Сохранить упражнение</button>@if (editing.Id != 0) { <button type="button" @onclick="ResetEditor">Отмена</button> }</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
<div class="list-toolbar">
|
||||
<h2>@ListTitle</h2>
|
||||
<input class="search" @bind="search" @bind:event="oninput" placeholder="Найти упражнение…" aria-label="Поиск упражнений" />
|
||||
</div>
|
||||
@if (selectedGroupId > 0)
|
||||
{
|
||||
<details class="panel compact">
|
||||
<summary>Добавить копию упражнения из библиотеки</summary>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)" class="add-exercise-bar">
|
||||
<ExercisePicker ResetVersion="pickerResetVersion" Id="group-exercise-picker" @key="selectedGroupId" Exercises="AvailableExercises" @bind-Value="existingExerciseId" OnApply="ApplyExistingAsync" />
|
||||
<button disabled="@(existingExerciseId == 0)" @onclick="AddExistingAsync">Добавить копию в группу</button>
|
||||
</fieldset>
|
||||
</details>
|
||||
}
|
||||
@foreach (var exercise in FilteredExercises)
|
||||
{
|
||||
<div class="program-row library-exercise" @key="exercise.Id" draggable="@(!Busy && RendererInfo.IsInteractive ? "true" : "false")"
|
||||
@ondragstart="() => draggedExerciseId = exercise.Id" @ondragend="() => draggedExerciseId = null">
|
||||
<div><strong>@exercise.Name</strong><p class="muted">@(exercise.Group?.Name ?? "Без группы")</p></div>
|
||||
<div class="actions">@if (!string.IsNullOrEmpty(exercise.VideoUrl)) { <a href="@exercise.VideoUrl" target="_blank" rel="noopener noreferrer">Видео ↗</a> }<button disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="() => Edit(exercise)">Изменить</button>
|
||||
<button class="icon-button danger-text" title="Удалить из библиотеки" aria-label="@($"Удалить упражнение {exercise.Name}")"
|
||||
disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="() => DeleteAsync(exercise)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (FilteredExercises.Count == 0)
|
||||
{
|
||||
<p class="muted">В этом списке пока нет упражнений, подходящих под запрос.</p>
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using TrainingPlanner.Application;
|
||||
using TrainingPlanner.Domain;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class Exercises
|
||||
{
|
||||
[Inject] public IExerciseRepository ExerciseLibrary { get; set; } = null!;
|
||||
[Inject] public IExerciseGroupRepository GroupRepository { get; set; } = null!;
|
||||
[Inject] public IJSRuntime JavaScript { get; set; } = null!;
|
||||
private List<LibraryExercise> items = [];
|
||||
private List<ExerciseGroup> groups = [];
|
||||
private LibraryExercise editing = new();
|
||||
private string search = string.Empty;
|
||||
private int selectedGroupId = -1;
|
||||
private int existingExerciseId;
|
||||
private int pickerResetVersion;
|
||||
private int? draggedExerciseId;
|
||||
private bool importOpen;
|
||||
private string importMessage = string.Empty;
|
||||
private Task ImportedAsync(int count)
|
||||
{
|
||||
importOpen = false;
|
||||
importMessage = $"Импортировано упражнений: {count}.";
|
||||
SelectGroup(-1);
|
||||
return RunAsync(ReloadAsync);
|
||||
}
|
||||
private List<LibraryExercise> AvailableExercises => items.Where(item => item.GroupId != selectedGroupId).ToList();
|
||||
private string ListTitle => selectedGroupId < 0 ? "Все упражнения" : selectedGroupId == 0 ? "Без группы" :
|
||||
groups.SingleOrDefault(group => group.Id == selectedGroupId)?.Name ?? "Группа";
|
||||
private List<LibraryExercise> FilteredExercises => ExerciseSearch.Find(items.Where(item =>
|
||||
selectedGroupId < 0 || (selectedGroupId == 0 ? item.GroupId == null : item.GroupId == selectedGroupId)), search);
|
||||
|
||||
protected override Task OnInitializedAsync() => RunAsync(ReloadAsync);
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
items = await ExerciseLibrary.GetAllAsync();
|
||||
groups = await GroupRepository.GetAllAsync();
|
||||
if (selectedGroupId > 0 && groups.All(group => group.Id != selectedGroupId)) selectedGroupId = -1;
|
||||
if (editing.Id != 0 && items.All(item => item.Id != editing.Id)) ResetEditor();
|
||||
if (editing.GroupId.HasValue && groups.All(group => group.Id != editing.GroupId)) editing.GroupId = null;
|
||||
}
|
||||
private void SelectGroup(int id)
|
||||
{
|
||||
selectedGroupId = id;
|
||||
existingExerciseId = 0;
|
||||
search = string.Empty;
|
||||
if (editing.Id == 0) ResetEditor();
|
||||
}
|
||||
private void ResetEditor() => editing = new LibraryExercise
|
||||
{
|
||||
GroupId = selectedGroupId > 0 ? selectedGroupId : null
|
||||
};
|
||||
private void Edit(LibraryExercise exercise) => editing = new LibraryExercise
|
||||
{
|
||||
Id = exercise.Id, Name = exercise.Name, VideoUrl = exercise.VideoUrl, GroupId = exercise.GroupId
|
||||
};
|
||||
private Task SaveAsync() => RunAsync(async () =>
|
||||
{
|
||||
await ExerciseLibrary.SaveAsync(editing);
|
||||
await ReloadAsync();
|
||||
ResetEditor();
|
||||
});
|
||||
private Task ApplyExistingAsync(int id)
|
||||
{
|
||||
existingExerciseId = id;
|
||||
return AddExistingAsync();
|
||||
}
|
||||
private Task AddExistingAsync() => RunAsync(async () =>
|
||||
{
|
||||
await GroupRepository.AddExerciseAsync(selectedGroupId, existingExerciseId);
|
||||
existingExerciseId = 0;
|
||||
pickerResetVersion++;
|
||||
await ReloadAsync();
|
||||
});
|
||||
private async Task MoveToGroupAsync(int targetGroupId)
|
||||
{
|
||||
var id = draggedExerciseId;
|
||||
draggedExerciseId = null;
|
||||
if (id == null || Busy || targetGroupId < 0) return;
|
||||
int? groupId = targetGroupId == 0 ? null : targetGroupId;
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
await ExerciseLibrary.MoveAsync(id.Value, groupId);
|
||||
if (editing.Id == id.Value) editing.GroupId = groupId;
|
||||
await ReloadAsync();
|
||||
});
|
||||
}
|
||||
private async Task DeleteAsync(LibraryExercise exercise)
|
||||
{
|
||||
if (Busy) return;
|
||||
if (!await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить «{exercise.Name}» из библиотеки? В составленных тренировках упражнение сохранится.")) return;
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
await ExerciseLibrary.DeleteAsync(exercise.Id);
|
||||
if (editing.Id == exercise.Id) ResetEditor();
|
||||
await ReloadAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@page "/"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@inherits UiPageBase
|
||||
@inject IClientRepository Clients
|
||||
<PageTitle>Клиенты · Training Planner</PageTitle>
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">РАБОЧЕЕ ПРОСТРАНСТВО</p>
|
||||
<h1>Клиенты</h1>
|
||||
<p class="muted">Персональные программы и история тренировок.</p>
|
||||
</div>
|
||||
<span class="count-badge">Клиентов: @items.Count</span>
|
||||
</header>
|
||||
@if (!string.IsNullOrEmpty(Error)) { <div class="error" role="alert">@Error</div> }
|
||||
<section class="panel compact">
|
||||
<h2>Новый клиент</h2>
|
||||
<form @onsubmit="CreateAsync" @onsubmit:preventDefault>
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)">
|
||||
<div class="inline-form">
|
||||
<label class="grow">Имя клиента<input @bind="name" placeholder="Имя и фамилия" required />
|
||||
</label>
|
||||
<button class="primary" disabled="@Busy">Добавить клиента</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
@if (items.Count == 0) { <section class="empty-state">
|
||||
<span class="empty-symbol">◎</span>
|
||||
<h2>Начните с первого клиента</h2>
|
||||
<p class="muted">После добавления клиента можно создать его программу на месяц.</p>
|
||||
</section> }
|
||||
else { <div class="client-grid">@foreach (var client in items) { <a class="client-card" href="@($"/clients/{client.Id}")">
|
||||
<span class="avatar">@client.Name[..1].ToUpperInvariant()</span>
|
||||
<div>
|
||||
<h2>@client.Name</h2>
|
||||
<p class="muted">Открыть программы</p>
|
||||
</div>
|
||||
<span class="arrow">↗</span>
|
||||
</a> }</div> }
|
||||
@code {
|
||||
private List<Client> items = [];
|
||||
private string name = string.Empty;
|
||||
protected override Task OnInitializedAsync() => RunAsync(async () => items = await Clients.GetAllAsync());
|
||||
private Task CreateAsync() => RunAsync(async () =>
|
||||
{
|
||||
await Clients.SaveAsync(new Client { Name = name });
|
||||
name = string.Empty;
|
||||
items = await Clients.GetAllAsync();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@page "/not-found"
|
||||
<PageTitle>Страница не найдена · Training Planner</PageTitle>
|
||||
<h1>Страница не найдена</h1>
|
||||
<a class="button" href="/">К клиентам</a>
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class ProgramEditor
|
||||
{
|
||||
private CancellationTokenSource? pendingUndoExpiry;
|
||||
|
||||
private Task DeleteBlockAsync(int index) => ModifyAsync(() =>
|
||||
{
|
||||
ClearDeletedBlock();
|
||||
deletedFrom = CurrentWorkout;
|
||||
deletedIndex = index;
|
||||
deletedBlock = CurrentWorkout!.Blocks[index];
|
||||
CurrentWorkout.Blocks.RemoveAt(index);
|
||||
pendingUndoExpiry = new CancellationTokenSource();
|
||||
_ = ExpireUndoAsync(pendingUndoExpiry.Token);
|
||||
});
|
||||
|
||||
private Task UndoDeleteAsync()
|
||||
{
|
||||
if (deletedBlock == null || deletedFrom != CurrentWorkout) return Task.CompletedTask;
|
||||
return ModifyAsync(() =>
|
||||
{
|
||||
deletedFrom!.Blocks.Insert(Math.Min(deletedIndex, deletedFrom.Blocks.Count), deletedBlock);
|
||||
ClearDeletedBlock();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ExpireUndoAsync(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(Options.UndoDeleteDurationSeconds), cancellation);
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
if (disposed || cancellation.IsCancellationRequested) return;
|
||||
ClearDeletedBlock();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private void ClearDeletedBlock()
|
||||
{
|
||||
pendingUndoExpiry?.Cancel();
|
||||
pendingUndoExpiry?.Dispose();
|
||||
pendingUndoExpiry = null;
|
||||
deletedBlock = null;
|
||||
deletedFrom = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using TrainingPlanner.Domain;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class ProgramEditor
|
||||
{
|
||||
private TrainingWeek? draggedWeek;
|
||||
|
||||
private async Task DeleteWeekAsync()
|
||||
{
|
||||
if (plan == null || Busy || plan.Weeks.Count <= 1) return;
|
||||
|
||||
var week = CurrentWeek;
|
||||
var confirmed = await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить неделю {weekIndex + 1} и все её тренировки?");
|
||||
if (!confirmed || Busy || plan.Weeks.Count <= 1 || !plan.Weeks.Contains(week)) return;
|
||||
|
||||
await ModifyAsync(() =>
|
||||
{
|
||||
var index = plan.Weeks.IndexOf(week);
|
||||
plan.Weeks.RemoveAt(index);
|
||||
for (var position = 0; position < plan.Weeks.Count; position++)
|
||||
plan.Weeks[position].Position = position;
|
||||
|
||||
SelectWeek(Math.Min(index, plan.Weeks.Count - 1));
|
||||
draggedWeek = null;
|
||||
dragIndex = -1;
|
||||
if (deletedFrom != null && week.Workouts.Contains(deletedFrom))
|
||||
{
|
||||
ClearDeletedBlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void StartWeekDrag(int index)
|
||||
{
|
||||
draggedWorkout = null;
|
||||
draggedWeek = plan!.Weeks[index];
|
||||
dragIndex = -1;
|
||||
}
|
||||
|
||||
private async Task DropWeekAsync(int targetIndex, DragEventArgs args)
|
||||
{
|
||||
var week = draggedWeek;
|
||||
draggedWeek = null;
|
||||
if (plan == null || week == null || Busy)
|
||||
return;
|
||||
|
||||
var sourceIndex = plan.Weeks.IndexOf(week);
|
||||
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= plan.Weeks.Count)
|
||||
return;
|
||||
|
||||
var insertionIndex = await JavaScript.InvokeAsync<int>("trainingPlanner.weekInsertionIndex", targetIndex, args.ClientX);
|
||||
if (insertionIndex < 0 || insertionIndex > plan.Weeks.Count) return;
|
||||
if (sourceIndex < insertionIndex) insertionIndex--;
|
||||
if (sourceIndex == insertionIndex) return;
|
||||
|
||||
await ModifyAsync(() =>
|
||||
{
|
||||
var selectedWeek = CurrentWeek;
|
||||
plan.Weeks.RemoveAt(sourceIndex);
|
||||
plan.Weeks.Insert(insertionIndex, week);
|
||||
for (var index = 0; index < plan.Weeks.Count; index++)
|
||||
plan.Weeks[index].Position = index;
|
||||
|
||||
weekIndex = plan.Weeks.IndexOf(selectedWeek);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using TrainingPlanner.Domain;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class ProgramEditor
|
||||
{
|
||||
private Workout? draggedWorkout;
|
||||
|
||||
private void StartWorkoutDrag(int index)
|
||||
{
|
||||
draggedWorkout = CurrentWeek.Workouts[index];
|
||||
draggedWeek = null;
|
||||
dragIndex = -1;
|
||||
}
|
||||
|
||||
private async Task DropWorkoutAsync(int targetIndex, DragEventArgs args)
|
||||
{
|
||||
var workout = draggedWorkout;
|
||||
draggedWorkout = null;
|
||||
if (plan == null || workout == null || Busy) return;
|
||||
|
||||
var week = CurrentWeek;
|
||||
var sourceIndex = week.Workouts.IndexOf(workout);
|
||||
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= week.Workouts.Count) return;
|
||||
|
||||
var insertionIndex = await JavaScript.InvokeAsync<int>(
|
||||
"trainingPlanner.workoutInsertionIndex", targetIndex, args.ClientY);
|
||||
if (Busy || CurrentWeek != week || insertionIndex < 0 || insertionIndex > week.Workouts.Count) return;
|
||||
if (sourceIndex < insertionIndex) insertionIndex--;
|
||||
if (sourceIndex == insertionIndex) return;
|
||||
|
||||
await ModifyAsync(() =>
|
||||
{
|
||||
var selectedWorkout = CurrentWorkout;
|
||||
week.Workouts.RemoveAt(sourceIndex);
|
||||
week.Workouts.Insert(insertionIndex, workout);
|
||||
for (var index = 0; index < week.Workouts.Count; index++)
|
||||
week.Workouts[index].Position = index;
|
||||
|
||||
workoutIndex = week.Workouts.IndexOf(selectedWorkout!);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class ProgramEditor
|
||||
{
|
||||
private async Task DeleteWorkoutAsync()
|
||||
{
|
||||
if (plan == null || Busy || CurrentWorkout == null) return;
|
||||
|
||||
var week = CurrentWeek;
|
||||
var workout = CurrentWorkout;
|
||||
var confirmed = await JavaScript.InvokeAsync<bool>("confirm",
|
||||
$"Удалить тренировку «{workout.Name}» и все её упражнения?");
|
||||
if (!confirmed || Busy || !plan.Weeks.Contains(week) || !week.Workouts.Contains(workout)) return;
|
||||
|
||||
await ModifyAsync(() =>
|
||||
{
|
||||
var index = week.Workouts.IndexOf(workout);
|
||||
week.Workouts.RemoveAt(index);
|
||||
for (var position = 0; position < week.Workouts.Count; position++)
|
||||
week.Workouts[position].Position = position;
|
||||
|
||||
weekIndex = plan.Weeks.IndexOf(week);
|
||||
workoutIndex = Math.Max(0, Math.Min(index, week.Workouts.Count - 1));
|
||||
dragIndex = -1;
|
||||
if (deletedFrom == workout)
|
||||
{
|
||||
ClearDeletedBlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@page "/programs/{Id:int}"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@inherits UiPageBase
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>@(plan?.Name ?? "Программа") · Training Planner</PageTitle>
|
||||
<NavigationLock ConfirmExternalNavigation="dirty" OnBeforeInternalNavigation="BeforeNavigation" />
|
||||
|
||||
@if (plan != null)
|
||||
{
|
||||
<a class="breadcrumb" href="@($"/clients/{plan.ClientId}")">← @clientName · Все программы</a>
|
||||
<header class="page-heading editor-heading">
|
||||
<div>
|
||||
<p class="eyebrow">ПРОГРАММА НА МЕСЯЦ</p>
|
||||
<h1>
|
||||
<input class="title-input" disabled="@(Busy || !RendererInfo.IsInteractive)"
|
||||
aria-label="Название программы" @bind="plan.Name"
|
||||
@bind:event="oninput" @bind:after="ChangedAsync" />
|
||||
</h1>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="save-status" role="status">
|
||||
@(Busy ? "Сохраняем…" : dirty ? "Есть несохранённые изменения" : "✓ Сохранено")
|
||||
</span>
|
||||
<button disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="SaveAsync">Сохранить</button>
|
||||
<button class="primary" disabled="@(Busy || !RendererInfo.IsInteractive)" @onclick="ExportAsync">
|
||||
↓ Скачать PDF
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Error))
|
||||
{
|
||||
<div class="error" role="alert">@Error</div>
|
||||
}
|
||||
|
||||
@if (plan != null)
|
||||
{
|
||||
<fieldset disabled="@(Busy || !RendererInfo.IsInteractive)" class="editor-fieldset">
|
||||
<div class="week-tabs" role="tablist" aria-label="Недели программы">
|
||||
@for (var index = 0; index < plan.Weeks.Count; index++)
|
||||
{
|
||||
var selectedIndex = index;
|
||||
<button @key="plan.Weeks[index]" role="tab" aria-selected="@(weekIndex == index ? "true" : "false")"
|
||||
data-week-index="@index"
|
||||
class="@(weekIndex == index ? "selected" : "")"
|
||||
draggable="true" title="Перетащите, чтобы изменить порядок недель"
|
||||
@ondragstart="() => StartWeekDrag(selectedIndex)"
|
||||
@ondragend="() => draggedWeek = null"
|
||||
@ondragover:preventDefault
|
||||
@ondrop:preventDefault
|
||||
@ondrop="args => DropWeekAsync(selectedIndex, args)"
|
||||
@onclick="() => SelectWeek(selectedIndex)">
|
||||
Неделя @(index + 1)
|
||||
<span>Тренировок: @plan.Weeks[index].Workouts.Count</span>
|
||||
</button>
|
||||
}
|
||||
<button class="add-week" @onclick="AddWeekAsync">+ Неделя</button>
|
||||
</div>
|
||||
|
||||
<div class="program-workspace">
|
||||
<aside class="workout-nav">
|
||||
<div class="list-toolbar">
|
||||
<h2>Тренировки</h2>
|
||||
<button class="icon-button" @onclick="AddWorkoutAsync" aria-label="Добавить тренировку">+</button>
|
||||
</div>
|
||||
@for (var index = 0; index < CurrentWeek.Workouts.Count; index++)
|
||||
{
|
||||
var selectedIndex = index;
|
||||
<button @key="CurrentWeek.Workouts[index]" class="workout-tab @(workoutIndex == index ? "selected" : "")"
|
||||
data-workout-index="@index" draggable="true"
|
||||
title="Перетащите, чтобы изменить порядок тренировок"
|
||||
@ondragstart="() => StartWorkoutDrag(selectedIndex)"
|
||||
@ondragend="() => draggedWorkout = null"
|
||||
@ondragover:preventDefault @ondrop:preventDefault
|
||||
@ondrop="args => DropWorkoutAsync(selectedIndex, args)"
|
||||
@onclick="() => workoutIndex = selectedIndex">
|
||||
<span>@((index + 1).ToString("00"))</span>
|
||||
<strong>@CurrentWeek.Workouts[index].Name</strong>
|
||||
</button>
|
||||
}
|
||||
<button class="text-button" @onclick="CopyWeekAsync">Копировать неделю</button>
|
||||
<button class="text-button danger-text" disabled="@(plan.Weeks.Count == 1)"
|
||||
title="@(plan.Weeks.Count == 1 ? "В программе должна остаться хотя бы одна неделя" : "Удалить выбранную неделю вместе с тренировками")"
|
||||
@onclick="DeleteWeekAsync">Удалить неделю</button>
|
||||
</aside>
|
||||
|
||||
<section class="workout-editor">
|
||||
@if (CurrentWorkout == null)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="empty-symbol">+</span>
|
||||
<h2>Первая тренировка недели</h2>
|
||||
<p class="muted">Добавьте тренировку, затем выберите упражнения.</p>
|
||||
<button class="primary" @onclick="AddWorkoutAsync">Добавить тренировку</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="workout-heading">
|
||||
<input aria-label="Название тренировки" @bind="CurrentWorkout.Name"
|
||||
@bind:event="oninput" @bind:after="ChangedAsync" />
|
||||
<button @onclick="CopyWorkoutAsync">Копировать тренировку</button>
|
||||
<button class="icon-button danger-text" title="Удалить тренировку"
|
||||
aria-label="Удалить тренировку" @onclick="DeleteWorkoutAsync">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="workout-summary">
|
||||
<p class="muted">Общий отдых: @WorkoutTotals.RestMinutes(CurrentWorkout).ToString("0.##") мин</p>
|
||||
<label>Время тренировки
|
||||
<input @bind="CurrentWorkout.Duration" @bind:event="oninput" @bind:after="ChangedAsync"
|
||||
placeholder="Например, 1 ч 15 мин" />
|
||||
</label>
|
||||
</div>
|
||||
@if (deletedBlock != null && deletedFrom == CurrentWorkout)
|
||||
{
|
||||
<div class="undo-message">
|
||||
Блок удалён.
|
||||
<button class="text-button" @onclick="UndoDeleteAsync">Отменить</button>
|
||||
</div>
|
||||
}
|
||||
@for (var index = 0; index < CurrentWorkout.Blocks.Count; index++)
|
||||
{
|
||||
var blockIndex = index;
|
||||
var block = CurrentWorkout.Blocks[index];
|
||||
<div @key="block" @ondragover:preventDefault @ondrop="() => DropAsync(blockIndex)">
|
||||
<BlockCard Block="block" Number="@(index + 1)"
|
||||
IsFirst="@(index == 0)" IsLast="@(index == CurrentWorkout.Blocks.Count - 1)"
|
||||
OnChanged="ChangedAsync" OnMove="direction => MoveAsync(blockIndex, direction)"
|
||||
OnCopy="() => CopyBlockAsync(blockIndex)" OnDelete="() => DeleteBlockAsync(blockIndex)"
|
||||
OnDrag="() => { draggedWorkout = null; draggedWeek = null; dragIndex = blockIndex; }" />
|
||||
</div>
|
||||
}
|
||||
<div class="add-exercise-bar">
|
||||
<ExercisePicker ResetVersion="pickerResetVersion" Id="workout-exercise-picker" Exercises="library" @bind-Value="libraryId" OnApply="ApplyLibraryExerciseAsync" />
|
||||
<button disabled="@(libraryId == 0)" @onclick="AddFromLibraryAsync">Добавить</button>
|
||||
<button class="primary" @onclick="AddManualAsync">+ Ввести вручную</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.JSInterop;
|
||||
using TrainingPlanner.Application;
|
||||
using TrainingPlanner.Domain;
|
||||
|
||||
namespace TrainingPlanner.Web.Components.Pages;
|
||||
|
||||
public partial class ProgramEditor
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
[Inject] public IPlanRepository Plans { get; set; } = null!;
|
||||
[Inject] public IClientRepository Clients { get; set; } = null!;
|
||||
[Inject] public IExerciseRepository Exercises { get; set; } = null!;
|
||||
[Inject] public PlanFactory Factory { get; set; } = null!;
|
||||
[Inject] public PlanCopier Copier { get; set; } = null!;
|
||||
[Inject] public BlockEditor Blocks { get; set; } = null!;
|
||||
[Inject] public PlannerOptions Options { get; set; } = null!;
|
||||
[Inject] public NavigationManager Navigation { get; set; } = null!;
|
||||
[Inject] public IJSRuntime JavaScript { get; set; } = null!;
|
||||
private TrainingPlan? plan;
|
||||
private List<LibraryExercise> library = [];
|
||||
private string clientName = string.Empty;
|
||||
private int weekIndex;
|
||||
private int workoutIndex;
|
||||
private int libraryId;
|
||||
private int pickerResetVersion;
|
||||
private int dragIndex = -1;
|
||||
private bool dirty;
|
||||
private bool disposed;
|
||||
private int editVersion;
|
||||
private CancellationTokenSource? pendingSave;
|
||||
private WorkoutBlock? deletedBlock;
|
||||
private Workout? deletedFrom;
|
||||
private int deletedIndex;
|
||||
private TrainingWeek CurrentWeek => plan!.Weeks[weekIndex];
|
||||
private Workout? CurrentWorkout => CurrentWeek.Workouts.ElementAtOrDefault(workoutIndex);
|
||||
|
||||
protected override Task OnParametersSetAsync() => RunAsync(async () =>
|
||||
{
|
||||
pendingSave?.Cancel();
|
||||
ClearDeletedBlock();
|
||||
plan = await Plans.GetAsync(Id) ?? throw new InvalidOperationException("Программа не найдена.");
|
||||
clientName = (await Clients.GetAsync(plan.ClientId))?.Name ?? string.Empty;
|
||||
library = await Exercises.GetAllAsync();
|
||||
weekIndex = 0;
|
||||
workoutIndex = 0;
|
||||
draggedWeek = null;
|
||||
draggedWorkout = null;
|
||||
dragIndex = -1;
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
private async Task ChangedAsync()
|
||||
{
|
||||
dirty = true;
|
||||
editVersion++;
|
||||
pendingSave?.Cancel();
|
||||
pendingSave?.Dispose();
|
||||
var cancellation = new CancellationTokenSource();
|
||||
pendingSave = cancellation;
|
||||
try
|
||||
{
|
||||
await Task.Delay(Options.AutosaveDelayMilliseconds, cancellation.Token);
|
||||
if (!disposed) await SaveAsync();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private Task SaveAsync()
|
||||
{
|
||||
pendingSave?.Cancel();
|
||||
return RunAsync(async () =>
|
||||
{
|
||||
var savedVersion = editVersion;
|
||||
var snapshot = Copier.Copy(plan!, plan!.Name);
|
||||
snapshot.Id = plan.Id;
|
||||
snapshot.Version = plan.Version;
|
||||
await Plans.SaveAsync(snapshot);
|
||||
plan.Version = snapshot.Version;
|
||||
plan.CreatedAtUtc = snapshot.CreatedAtUtc;
|
||||
plan.UpdatedAtUtc = snapshot.UpdatedAtUtc;
|
||||
dirty = savedVersion != editVersion;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
await SaveAsync();
|
||||
if (!dirty && string.IsNullOrEmpty(Error))
|
||||
await JavaScript.InvokeVoidAsync("trainingPlanner.download", $"/programs/{Id}/pdf");
|
||||
}
|
||||
|
||||
private async Task ModifyAsync(Action action)
|
||||
{
|
||||
try { action(); Error = string.Empty; }
|
||||
catch (InvalidOperationException exception) { Error = exception.Message; return; }
|
||||
await ChangedAsync();
|
||||
}
|
||||
|
||||
private void SelectWeek(int index) { weekIndex = index; workoutIndex = 0; draggedWorkout = null; dragIndex = -1; }
|
||||
private Task AddWeekAsync() => ModifyAsync(() =>
|
||||
{
|
||||
plan!.Weeks.Add(new TrainingWeek());
|
||||
SelectWeek(plan.Weeks.Count - 1);
|
||||
});
|
||||
private Task CopyWeekAsync() => ModifyAsync(() =>
|
||||
{
|
||||
plan!.Weeks.Add(Copier.CopyWeek(CurrentWeek));
|
||||
SelectWeek(plan.Weeks.Count - 1);
|
||||
});
|
||||
private Task AddWorkoutAsync() => ModifyAsync(() =>
|
||||
{
|
||||
CurrentWeek.Workouts.Add(new Workout
|
||||
{
|
||||
Name = $"{Options.DefaultWorkoutName} {CurrentWeek.Workouts.Count + 1}",
|
||||
Duration = Options.DefaultWorkoutDuration
|
||||
});
|
||||
workoutIndex = CurrentWeek.Workouts.Count - 1;
|
||||
});
|
||||
private Task CopyWorkoutAsync() => ModifyAsync(() =>
|
||||
{
|
||||
CurrentWeek.Workouts.Add(Copier.CopyWorkout(CurrentWorkout!));
|
||||
workoutIndex = CurrentWeek.Workouts.Count - 1;
|
||||
});
|
||||
private Task AddManualAsync() => ModifyAsync(() => CurrentWorkout!.Blocks.Add(Factory.CreateBlock()));
|
||||
private Task ApplyLibraryExerciseAsync(int id)
|
||||
{
|
||||
libraryId = id;
|
||||
return AddFromLibraryAsync();
|
||||
}
|
||||
private Task AddFromLibraryAsync() => ModifyAsync(() =>
|
||||
{
|
||||
CurrentWorkout!.Blocks.Add(Factory.CreateBlock(library.Single(exercise => exercise.Id == libraryId)));
|
||||
libraryId = 0;
|
||||
pickerResetVersion++;
|
||||
});
|
||||
private Task MoveAsync(int index, int direction) => ModifyAsync(() => Blocks.Move(CurrentWorkout!, index, index + direction));
|
||||
private Task DropAsync(int index) => ModifyAsync(() => { Blocks.Move(CurrentWorkout!, dragIndex, index); dragIndex = -1; });
|
||||
private Task CopyBlockAsync(int index) => ModifyAsync(() => CurrentWorkout!.Blocks.Insert(index + 1, Copier.CopyBlock(CurrentWorkout.Blocks[index])));
|
||||
private async Task BeforeNavigation(LocationChangingContext context)
|
||||
{
|
||||
if (dirty && !await JavaScript.InvokeAsync<bool>("confirm", "Есть несохранённые изменения. Покинуть программу?"))
|
||||
context.PreventNavigation();
|
||||
}
|
||||
public void Dispose() { disposed = true; ClearDeletedBlock(); pendingSave?.Cancel(); pendingSave?.Dispose(); }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@inject NavigationManager Navigation
|
||||
@code {
|
||||
protected override void OnInitialized() => Navigation.NavigateTo("/Login", forceLoad: true);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace TrainingPlanner.Web.Components;
|
||||
|
||||
public class UiPageBase : ComponentBase
|
||||
{
|
||||
[Inject] public ILogger<UiPageBase> Logger { get; set; } = null!;
|
||||
protected string Error { get; set; } = string.Empty;
|
||||
protected bool Busy { get; set; }
|
||||
|
||||
protected async Task RunAsync(Func<Task> action)
|
||||
{
|
||||
if (Busy) return;
|
||||
Busy = true;
|
||||
Error = string.Empty;
|
||||
try { await action(); }
|
||||
catch (InvalidOperationException exception) { Error = exception.Message; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.LogError(exception, "Operation failed");
|
||||
Error = "Не удалось выполнить действие. Изменения остались на экране. Попробуйте ещё раз.";
|
||||
}
|
||||
finally { Busy = false; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using TrainingPlanner.Web
|
||||
@using TrainingPlanner.Web.Components
|
||||
@using TrainingPlanner.Web.Components.Layout
|
||||
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using TrainingPlanner.Application
|
||||
@using TrainingPlanner.Domain
|
||||
@using TrainingPlanner.Web.Components.Editor
|
||||
|
||||
@using TrainingPlanner.Web.Components.Library
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrainingPlanner.Application;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
using TrainingPlanner.Infrastructure.Pdf;
|
||||
using TrainingPlanner.Web.Security;
|
||||
|
||||
namespace TrainingPlanner.Web.Configuration;
|
||||
|
||||
public static class ServiceRegistration
|
||||
{
|
||||
public static void AddTrainingPlanner(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddOptions<ExerciseImportOptions>().Bind(config.GetSection("ExerciseImport"))
|
||||
.Validate(options => options.MaxRows > 0 && options.MaxClipboardCharacters > 0,
|
||||
"Проверьте настройки ExerciseImport.").ValidateOnStart();
|
||||
services.AddSingleton(provider => provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<ExerciseImportOptions>>().Value);
|
||||
services.AddSingleton<ClipboardTableParser>();
|
||||
services.AddSingleton<ExerciseImportValidator>();
|
||||
services.AddScoped<IExerciseImporter, ExerciseImporter>();
|
||||
services.AddOptions<PlannerOptions>().Bind(config.GetSection("Planner"))
|
||||
.Validate(options => options.DefaultWeekCount > 0 && options.DefaultSetsCount > 0
|
||||
&& options.DefaultRestMinutes >= 0 && options.AutosaveDelayMilliseconds > 0 && options.UndoDeleteDurationSeconds > 0
|
||||
&& options.DefaultIntensityPercent is >= 0 and <= 100
|
||||
&& !string.IsNullOrWhiteSpace(options.DefaultWorkoutDuration),
|
||||
"Проверьте настройки Planner.").ValidateOnStart();
|
||||
services.AddSingleton(provider => provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<PlannerOptions>>().Value);
|
||||
services.AddDbContextFactory<TrainingDbContext>(options => options.UseNpgsql(
|
||||
config.GetConnectionString("TrainingDatabase"), database => database.MigrationsAssembly("TrainingPlanner.Web")));
|
||||
services.AddIdentity<IdentityUser, IdentityRole>(options => config.GetSection("Identity").Bind(options))
|
||||
.AddEntityFrameworkStores<TrainingDbContext>().AddDefaultTokenProviders();
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/Login";
|
||||
options.AccessDeniedPath = "/Login";
|
||||
options.ExpireTimeSpan = TimeSpan.FromHours(config.GetValue<double>("Security:CookieHours"));
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.Cookie.SecurePolicy = config.GetValue<bool>("Security:RequireHttps")
|
||||
? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
});
|
||||
var keyPath = config["Security:DataProtectionKeysPath"];
|
||||
if (!string.IsNullOrWhiteSpace(keyPath))
|
||||
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(keyPath));
|
||||
services.AddCascadingAuthenticationState();
|
||||
services.AddScoped<AuthenticationStateProvider, TrainerAuthenticationStateProvider>();
|
||||
services.AddScoped<ITrainerContext, TrainerContext>();
|
||||
services.AddScoped<IClientRepository, ClientRepository>();
|
||||
services.AddScoped<IExerciseRepository, ExerciseRepository>();
|
||||
services.AddScoped<IExerciseGroupRepository, ExerciseGroupRepository>();
|
||||
services.AddScoped<IPlanRepository, PlanRepository>();
|
||||
services.AddScoped<IPlanReader, PlanReader>();
|
||||
services.AddScoped<DatabaseInitializer>();
|
||||
services.AddSingleton<PlanFactory>();
|
||||
services.AddSingleton<PlanCopier>();
|
||||
services.AddSingleton<PlanValidator>();
|
||||
services.AddSingleton<BlockEditor>();
|
||||
services.AddSingleton<IPlanPdfExporter, PlanPdfExporter>();
|
||||
services.AddSingleton(config.GetSection("Pdf").Get<PdfOptions>() ?? new PdfOptions());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
namespace TrainingPlanner.Web.Data;
|
||||
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<TrainingDbContext>
|
||||
{
|
||||
public TrainingDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<TrainingDbContext>();
|
||||
builder.UseNpgsql(options => options.MigrationsAssembly(typeof(Program).Assembly.GetName().Name));
|
||||
return new TrainingDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
+602
@@ -0,0 +1,602 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TrainingDbContext))]
|
||||
[Migration("20260924164145_InitialSchema")]
|
||||
partial class InitialSchema
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("Clients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseSet", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Repetitions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("WeightKg")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("WorkoutExerciseId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutExerciseId");
|
||||
|
||||
b.ToTable("ExerciseSet");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("LibraryExercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("TrainingPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingPlanId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingPlanId");
|
||||
|
||||
b.ToTable("TrainingWeek");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingWeekId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingWeekId");
|
||||
|
||||
b.ToTable("Workout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RestSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkoutId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutId");
|
||||
|
||||
b.ToTable("WorkoutBlock");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("RequiresVideo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("WorkoutBlockId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutBlockId");
|
||||
|
||||
b.ToTable("WorkoutExercise");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseSet", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutExercise", null)
|
||||
.WithMany("Sets")
|
||||
.HasForeignKey("WorkoutExerciseId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Client", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingPlan", null)
|
||||
.WithMany("Weeks")
|
||||
.HasForeignKey("TrainingPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingWeek", null)
|
||||
.WithMany("Workouts")
|
||||
.HasForeignKey("TrainingWeekId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Workout", null)
|
||||
.WithMany("Blocks")
|
||||
.HasForeignKey("WorkoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutBlock", null)
|
||||
.WithMany("Exercises")
|
||||
.HasForeignKey("WorkoutBlockId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Navigation("Weeks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Navigation("Workouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Navigation("Exercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Navigation("Sets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialSchema : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Clients",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TrainerId = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Notes = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Clients", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Clients_AspNetUsers_TrainerId",
|
||||
column: x => x.TrainerId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryExercises",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TrainerId = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
VideoUrl = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryExercises", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_LibraryExercises_AspNetUsers_TrainerId",
|
||||
column: x => x.TrainerId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TrainingPlans",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ClientId = table.Column<int>(type: "integer", nullable: false),
|
||||
TrainerId = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TrainingPlans", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TrainingPlans_AspNetUsers_TrainerId",
|
||||
column: x => x.TrainerId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_TrainingPlans_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TrainingWeek",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TrainingPlanId = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TrainingWeek", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TrainingWeek_TrainingPlans_TrainingPlanId",
|
||||
column: x => x.TrainingPlanId,
|
||||
principalTable: "TrainingPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Workout",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TrainingWeekId = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Workout", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Workout_TrainingWeek_TrainingWeekId",
|
||||
column: x => x.TrainingWeekId,
|
||||
principalTable: "TrainingWeek",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkoutBlock",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
WorkoutId = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false),
|
||||
RestSeconds = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkoutBlock", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkoutBlock_Workout_WorkoutId",
|
||||
column: x => x.WorkoutId,
|
||||
principalTable: "Workout",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkoutExercise",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
WorkoutBlockId = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
VideoUrl = table.Column<string>(type: "text", nullable: false),
|
||||
RequiresVideo = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkoutExercise", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkoutExercise_WorkoutBlock_WorkoutBlockId",
|
||||
column: x => x.WorkoutBlockId,
|
||||
principalTable: "WorkoutBlock",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExerciseSet",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
WorkoutExerciseId = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false),
|
||||
Repetitions = table.Column<string>(type: "text", nullable: false),
|
||||
WeightKg = table.Column<decimal>(type: "numeric", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExerciseSet", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExerciseSet_WorkoutExercise_WorkoutExerciseId",
|
||||
column: x => x.WorkoutExerciseId,
|
||||
principalTable: "WorkoutExercise",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Clients_TrainerId",
|
||||
table: "Clients",
|
||||
column: "TrainerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExerciseSet_WorkoutExerciseId",
|
||||
table: "ExerciseSet",
|
||||
column: "WorkoutExerciseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LibraryExercises_TrainerId",
|
||||
table: "LibraryExercises",
|
||||
column: "TrainerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TrainingPlans_ClientId",
|
||||
table: "TrainingPlans",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TrainingPlans_TrainerId",
|
||||
table: "TrainingPlans",
|
||||
column: "TrainerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TrainingWeek_TrainingPlanId",
|
||||
table: "TrainingWeek",
|
||||
column: "TrainingPlanId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Workout_TrainingWeekId",
|
||||
table: "Workout",
|
||||
column: "TrainingWeekId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkoutBlock_WorkoutId",
|
||||
table: "WorkoutBlock",
|
||||
column: "WorkoutId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkoutExercise_WorkoutBlockId",
|
||||
table: "WorkoutExercise",
|
||||
column: "WorkoutBlockId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExerciseSet");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryExercises");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkoutExercise");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkoutBlock");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Workout");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TrainingWeek");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TrainingPlans");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Clients");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TrainingDbContext))]
|
||||
[Migration("20260924224103_ExercisePrescription")]
|
||||
partial class ExercisePrescription
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("Clients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("LibraryExercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("TrainingPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingPlanId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingPlanId");
|
||||
|
||||
b.ToTable("TrainingWeek");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingWeekId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingWeekId");
|
||||
|
||||
b.ToTable("Workout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("RestMinutes")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("SetsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkoutId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutId");
|
||||
|
||||
b.ToTable("WorkoutBlock");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("IntensityPercent")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Repetitions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("RequiresVideo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("WarmupSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Weight")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("WorkoutBlockId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutBlockId");
|
||||
|
||||
b.ToTable("WorkoutExercise");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Client", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingPlan", null)
|
||||
.WithMany("Weeks")
|
||||
.HasForeignKey("TrainingPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingWeek", null)
|
||||
.WithMany("Workouts")
|
||||
.HasForeignKey("TrainingWeekId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Workout", null)
|
||||
.WithMany("Blocks")
|
||||
.HasForeignKey("WorkoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutBlock", null)
|
||||
.WithMany("Exercises")
|
||||
.HasForeignKey("WorkoutBlockId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Navigation("Weeks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Navigation("Workouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Navigation("Exercises");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExercisePrescription : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SetsCount", table: "WorkoutBlock", type: "integer", nullable: false, defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "IntensityPercent",
|
||||
table: "WorkoutExercise",
|
||||
type: "numeric",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Notes",
|
||||
table: "WorkoutExercise",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Repetitions",
|
||||
table: "WorkoutExercise",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "WarmupSets",
|
||||
table: "WorkoutExercise",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Weight",
|
||||
table: "WorkoutExercise",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "RestMinutes",
|
||||
table: "WorkoutBlock",
|
||||
type: "numeric",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Duration",
|
||||
table: "Workout",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
// Preserve the first approach as the shared prescription before removing the old rows.
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "WorkoutExercise" AS exercise
|
||||
SET "Repetitions" = COALESCE((
|
||||
SELECT "Repetitions" FROM "ExerciseSet"
|
||||
WHERE "WorkoutExerciseId" = exercise."Id" ORDER BY "Position", "Id" LIMIT 1), ''),
|
||||
"Weight" = COALESCE((
|
||||
SELECT replace("WeightKg"::text, '.', ',') FROM "ExerciseSet"
|
||||
WHERE "WorkoutExerciseId" = exercise."Id" ORDER BY "Position", "Id" LIMIT 1), '');
|
||||
|
||||
UPDATE "WorkoutBlock" SET "RestMinutes" = "RestSeconds" / 60.0;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
item RECORD;
|
||||
target_block integer;
|
||||
previous_block integer := NULL;
|
||||
previous_workout integer := NULL;
|
||||
block_position integer := 0;
|
||||
BEGIN
|
||||
FOR item IN
|
||||
SELECT b."Id" AS block_id, b."WorkoutId" AS workout_id,
|
||||
b."RestMinutes" AS rest_minutes, b."RestSeconds" AS rest_seconds,
|
||||
e."Id" AS exercise_id,
|
||||
(SELECT count(*) FROM "ExerciseSet" s WHERE s."WorkoutExerciseId" = e."Id")::integer AS sets_count
|
||||
FROM "WorkoutBlock" b JOIN "WorkoutExercise" e ON e."WorkoutBlockId" = b."Id"
|
||||
ORDER BY b."WorkoutId", b."Position", b."Id", e."Position", e."Id"
|
||||
LOOP
|
||||
IF previous_workout IS DISTINCT FROM item.workout_id THEN
|
||||
block_position := 0;
|
||||
END IF;
|
||||
IF previous_block IS DISTINCT FROM item.block_id THEN
|
||||
target_block := item.block_id;
|
||||
UPDATE "WorkoutBlock" SET "Position" = block_position,
|
||||
"SetsCount" = GREATEST(1, item.sets_count) WHERE "Id" = target_block;
|
||||
ELSE
|
||||
INSERT INTO "WorkoutBlock" ("WorkoutId", "Position", "RestSeconds", "RestMinutes", "SetsCount")
|
||||
VALUES (item.workout_id, block_position, item.rest_seconds, item.rest_minutes, GREATEST(1, item.sets_count))
|
||||
RETURNING "Id" INTO target_block;
|
||||
END IF;
|
||||
UPDATE "WorkoutExercise" SET "WorkoutBlockId" = target_block, "Position" = 0
|
||||
WHERE "Id" = item.exercise_id;
|
||||
previous_block := item.block_id;
|
||||
previous_workout := item.workout_id;
|
||||
block_position := block_position + 1;
|
||||
END LOOP;
|
||||
END $$;
|
||||
""");
|
||||
migrationBuilder.DropTable(name: "ExerciseSet");
|
||||
migrationBuilder.DropColumn(name: "RestSeconds", table: "WorkoutBlock");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Автоматический откат не восстанавливает индивидуальные подходы. Восстановите резервную копию БД вместе с предыдущей версией приложения.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+630
@@ -0,0 +1,630 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TrainingDbContext))]
|
||||
[Migration("20260925193904_ExerciseGroups")]
|
||||
partial class ExerciseGroups
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("Clients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("ExerciseGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("GroupId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("LibraryExercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("TrainingPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingPlanId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingPlanId");
|
||||
|
||||
b.ToTable("TrainingWeek");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingWeekId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingWeekId");
|
||||
|
||||
b.ToTable("Workout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("RestMinutes")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("SetsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkoutId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutId");
|
||||
|
||||
b.ToTable("WorkoutBlock");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("IntensityPercent")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Repetitions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("RequiresVideo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("WarmupSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Weight")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("WorkoutBlockId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutBlockId");
|
||||
|
||||
b.ToTable("WorkoutExercise");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.ExerciseGroup", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Client", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingPlan", null)
|
||||
.WithMany("Weeks")
|
||||
.HasForeignKey("TrainingPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingWeek", null)
|
||||
.WithMany("Workouts")
|
||||
.HasForeignKey("TrainingWeekId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Workout", null)
|
||||
.WithMany("Blocks")
|
||||
.HasForeignKey("WorkoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutBlock", null)
|
||||
.WithMany("Exercises")
|
||||
.HasForeignKey("WorkoutBlockId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Navigation("Weeks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Navigation("Workouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Navigation("Exercises");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExerciseGroups : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GroupId",
|
||||
table: "LibraryExercises",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExerciseGroups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TrainerId = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExerciseGroups", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExerciseGroups_AspNetUsers_TrainerId",
|
||||
column: x => x.TrainerId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LibraryExercises_GroupId",
|
||||
table: "LibraryExercises",
|
||||
column: "GroupId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExerciseGroups_TrainerId",
|
||||
table: "ExerciseGroups",
|
||||
column: "TrainerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_LibraryExercises_ExerciseGroups_GroupId",
|
||||
table: "LibraryExercises",
|
||||
column: "GroupId",
|
||||
principalTable: "ExerciseGroups",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_LibraryExercises_ExerciseGroups_GroupId",
|
||||
table: "LibraryExercises");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExerciseGroups");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_LibraryExercises_GroupId",
|
||||
table: "LibraryExercises");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GroupId",
|
||||
table: "LibraryExercises");
|
||||
}
|
||||
}
|
||||
}
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TrainingDbContext))]
|
||||
[Migration("20260929161544_ClientProfile")]
|
||||
partial class ClientProfile
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Goal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("HealthStatus")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainingSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("Clients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("ExerciseGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("GroupId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("LibraryExercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("TrainingPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingPlanId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingPlanId");
|
||||
|
||||
b.ToTable("TrainingWeek");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingWeekId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingWeekId");
|
||||
|
||||
b.ToTable("Workout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("RestMinutes")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("SetsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkoutId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutId");
|
||||
|
||||
b.ToTable("WorkoutBlock");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("IntensityPercent")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Repetitions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("RequiresVideo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("WarmupSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Weight")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("WorkoutBlockId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutBlockId");
|
||||
|
||||
b.ToTable("WorkoutExercise");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.ExerciseGroup", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Client", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingPlan", null)
|
||||
.WithMany("Weeks")
|
||||
.HasForeignKey("TrainingPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingWeek", null)
|
||||
.WithMany("Workouts")
|
||||
.HasForeignKey("TrainingWeekId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Workout", null)
|
||||
.WithMany("Blocks")
|
||||
.HasForeignKey("WorkoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutBlock", null)
|
||||
.WithMany("Exercises")
|
||||
.HasForeignKey("WorkoutBlockId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Navigation("Weeks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Navigation("Workouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Navigation("Exercises");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ClientProfile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Goal",
|
||||
table: "Clients",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HealthStatus",
|
||||
table: "Clients",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TrainingSchedule",
|
||||
table: "Clients",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Goal",
|
||||
table: "Clients");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HealthStatus",
|
||||
table: "Clients");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TrainingSchedule",
|
||||
table: "Clients");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrainingPlanner.Web.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TrainingDbContext))]
|
||||
partial class TrainingDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Goal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("HealthStatus")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainingSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("Clients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("ExerciseGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("GroupId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("LibraryExercises");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TrainerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("TrainerId");
|
||||
|
||||
b.ToTable("TrainingPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingPlanId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingPlanId");
|
||||
|
||||
b.ToTable("TrainingWeek");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TrainingWeekId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TrainingWeekId");
|
||||
|
||||
b.ToTable("Workout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("RestMinutes")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("SetsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkoutId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutId");
|
||||
|
||||
b.ToTable("WorkoutBlock");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("IntensityPercent")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Repetitions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("RequiresVideo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("VideoUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("WarmupSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Weight")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("WorkoutBlockId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkoutBlockId");
|
||||
|
||||
b.ToTable("WorkoutExercise");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Client", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.ExerciseGroup", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.LibraryExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.ExerciseGroup", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Client", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TrainerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingPlan", null)
|
||||
.WithMany("Weeks")
|
||||
.HasForeignKey("TrainingPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.TrainingWeek", null)
|
||||
.WithMany("Workouts")
|
||||
.HasForeignKey("TrainingWeekId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.Workout", null)
|
||||
.WithMany("Blocks")
|
||||
.HasForeignKey("WorkoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutExercise", b =>
|
||||
{
|
||||
b.HasOne("TrainingPlanner.Domain.WorkoutBlock", null)
|
||||
.WithMany("Exercises")
|
||||
.HasForeignKey("WorkoutBlockId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingPlan", b =>
|
||||
{
|
||||
b.Navigation("Weeks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.TrainingWeek", b =>
|
||||
{
|
||||
b.Navigation("Workouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.Workout", b =>
|
||||
{
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrainingPlanner.Domain.WorkoutBlock", b =>
|
||||
{
|
||||
b.Navigation("Exercises");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@page "/programs/{id:int}/pdf"
|
||||
@model TrainingPlanner.Web.Pages.ExportModel
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TrainingPlanner.Application;
|
||||
|
||||
namespace TrainingPlanner.Web.Pages;
|
||||
|
||||
[Authorize]
|
||||
public class ExportModel(IPlanReader reader, IPlanPdfExporter exporter) : PageModel
|
||||
{
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
var trainerId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (trainerId == null) return Challenge();
|
||||
var plan = await reader.GetAsync(id, trainerId);
|
||||
if (plan == null) return NotFound();
|
||||
var client = await reader.GetClientAsync(plan.ClientId, trainerId);
|
||||
if (client == null) return NotFound();
|
||||
Response.Headers.CacheControl = "no-store";
|
||||
return File(exporter.Export(plan, client), "application/pdf", $"training-program-{id}.pdf");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@page
|
||||
@model TrainingPlanner.Web.Pages.LoginModel
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="icon" href="/favicon.svg"/><title>Вход · Training Planner</title><link rel="stylesheet" href="/app.css"/></head>
|
||||
<body class="login-page"><main class="login-card"><div class="brand-mark">TP</div><p class="eyebrow">РАБОЧЕЕ МЕСТО ТРЕНЕРА</p><h1>С возвращением</h1><p class="muted">Войдите, чтобы продолжить работу с программами.</p>
|
||||
<form method="post"><div asp-validation-summary="All" class="error" role="alert"></div>
|
||||
<label>Логин<input asp-for="UserName" autocomplete="username" required autofocus/></label>
|
||||
<label>Пароль<input asp-for="Password" type="password" autocomplete="current-password" required/></label>
|
||||
<button class="primary" type="submit">Войти →</button></form></main></body></html>
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace TrainingPlanner.Web.Pages;
|
||||
|
||||
[AllowAnonymous]
|
||||
public class LoginModel(SignInManager<IdentityUser> signIn) : PageModel
|
||||
{
|
||||
[BindProperty] public string UserName { get; set; } = string.Empty;
|
||||
[BindProperty] public string Password { get; set; } = string.Empty;
|
||||
|
||||
public IActionResult OnGet() => User.Identity?.IsAuthenticated == true ? LocalRedirect("/") : Page();
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string? returnUrl)
|
||||
{
|
||||
if (!ModelState.IsValid) return Page();
|
||||
var result = await signIn.PasswordSignInAsync(UserName, Password, false, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
return LocalRedirect(Url.IsLocalUrl(returnUrl) ? returnUrl! : "/");
|
||||
ModelState.AddModelError(string.Empty, result.IsLockedOut
|
||||
? "Слишком много попыток. Попробуйте позже." : "Не удалось войти. Проверьте логин и пароль.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@page
|
||||
@model TrainingPlanner.Web.Pages.LogoutModel
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace TrainingPlanner.Web.Pages;
|
||||
|
||||
[Authorize]
|
||||
public class LogoutModel(SignInManager<IdentityUser> signIn) : PageModel
|
||||
{
|
||||
public IActionResult OnGet() => LocalRedirect("/");
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await signIn.SignOutAsync();
|
||||
return LocalRedirect("/Login");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using TrainingPlanner.Infrastructure.Data;
|
||||
using TrainingPlanner.Web.Components;
|
||||
using TrainingPlanner.Web.Configuration;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents()
|
||||
.AddHubOptions(options => options.MaximumReceiveMessageSize =
|
||||
builder.Configuration.GetValue<long>("InteractiveServer:MaximumReceiveMessageBytes"));
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddTrainingPlanner(builder.Configuration);
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
foreach (var address in builder.Configuration.GetSection("Security:KnownProxies").Get<string[]>() ?? [])
|
||||
options.KnownProxies.Add(System.Net.IPAddress.Parse(address));
|
||||
});
|
||||
var app = builder.Build();
|
||||
app.UseForwardedHeaders();
|
||||
if (!app.Environment.IsDevelopment())
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
if (builder.Configuration.GetValue<bool>("Security:RequireHttps"))
|
||||
{
|
||||
app.UseHsts();
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorPages();
|
||||
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
||||
if (builder.Configuration.GetValue<bool>("Database:InitializeOnStartup"))
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>().InitializeAsync();
|
||||
}
|
||||
app.Run();
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TrainingPlanner.Web.Security;
|
||||
|
||||
public class TrainerAuthenticationStateProvider(ILoggerFactory loggerFactory,
|
||||
IServiceScopeFactory scopes, IConfiguration configuration)
|
||||
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
|
||||
{
|
||||
protected override TimeSpan RevalidationInterval =>
|
||||
TimeSpan.FromMinutes(configuration.GetValue<double>("Security:RevalidationMinutes"));
|
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = scopes.CreateScope();
|
||||
var users = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
|
||||
var user = await users.GetUserAsync(authenticationState.User);
|
||||
if (user == null) return false;
|
||||
var stamp = authenticationState.User.FindFirstValue(new IdentityOptions().ClaimsIdentity.SecurityStampClaimType);
|
||||
return stamp == await users.GetSecurityStampAsync(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using TrainingPlanner.Application;
|
||||
|
||||
namespace TrainingPlanner.Web.Security;
|
||||
|
||||
public class TrainerContext(AuthenticationStateProvider stateProvider) : ITrainerContext
|
||||
{
|
||||
public async Task<string> GetTrainerIdAsync()
|
||||
{
|
||||
var state = await stateProvider.GetAuthenticationStateAsync();
|
||||
return state.User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? throw new UnauthorizedAccessException("Войдите в приложение.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TrainingPlanner.Infrastructure\TrainingPlanner.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- Docker restores project files before Razor sources are copied. -->
|
||||
<RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
|
||||
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Security": {
|
||||
"RequireHttps": false
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"TrainingDatabase": ""
|
||||
},
|
||||
"Planner": {
|
||||
"DefaultWorkoutDuration": "1 час",
|
||||
"DefaultIntensityPercent": 80,
|
||||
"DefaultWeekCount": 4,
|
||||
"DefaultSetsCount": 3,
|
||||
"DefaultRepetitions": "10",
|
||||
"DefaultWorkoutName": "Тренировка",
|
||||
"UndoDeleteDurationSeconds": 10,
|
||||
"AutosaveDelayMilliseconds": 1200,
|
||||
"DefaultRestMinutes": 2
|
||||
},
|
||||
"BootstrapTrainer": {
|
||||
"UserName": "test",
|
||||
"Password": "test"
|
||||
},
|
||||
"Database": {
|
||||
"InitializeOnStartup": true
|
||||
},
|
||||
"Security": {
|
||||
"RequireHttps": true,
|
||||
"CookieHours": 8,
|
||||
"RevalidationMinutes": 5,
|
||||
"DataProtectionKeysPath": "",
|
||||
"KnownProxies": []
|
||||
},
|
||||
"Identity": {
|
||||
"Password": {
|
||||
"RequiredLength": 12,
|
||||
"RequireDigit": true,
|
||||
"RequireLowercase": true,
|
||||
"RequireUppercase": true,
|
||||
"RequireNonAlphanumeric": true
|
||||
},
|
||||
"Lockout": {
|
||||
"DefaultLockoutTimeSpan": "00:15:00",
|
||||
"MaxFailedAccessAttempts": 5,
|
||||
"AllowedForNewUsers": true
|
||||
}
|
||||
},
|
||||
"ExerciseImport": {
|
||||
"MaxRows": 1000,
|
||||
"MaxClipboardCharacters": 100000
|
||||
},
|
||||
"InteractiveServer": {
|
||||
"MaximumReceiveMessageBytes": 1048576
|
||||
},
|
||||
"Pdf": {
|
||||
"ClientDetailsGapLines": 5,
|
||||
"ClientLabelWidthCentimeters": 4.5,
|
||||
"FontPath": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"BoldFontPath": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"FontFamily": "PlannerSans",
|
||||
"FontSize": 8,
|
||||
"CameraSizePoints": 18,
|
||||
"HeadingSize": 16,
|
||||
"MarginCentimeters": 1.0,
|
||||
"AccentColor": "#6654CB",
|
||||
"HeaderBackground": "#E1E3E7",
|
||||
"BorderColor": "#E0E3EB",
|
||||
"ColumnWidthsCentimeters": [
|
||||
0.86,
|
||||
4.42,
|
||||
3.24,
|
||||
3.24,
|
||||
1.73,
|
||||
2.37,
|
||||
2.37,
|
||||
2.16,
|
||||
1.19,
|
||||
1.94,
|
||||
1.51,
|
||||
2.47
|
||||
],
|
||||
"WeekBackground": "#FFF15B",
|
||||
"WorkoutBackground": "#A7EB75"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,788 @@
|
||||
:root {
|
||||
font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
color:#202637;
|
||||
background:#f5f6fa;
|
||||
font-synthesis:none;
|
||||
--accent:#6654cb;
|
||||
--line:#e5e7ef;
|
||||
--muted:#737b8d
|
||||
}
|
||||
* {
|
||||
box-sizing:border-box
|
||||
}
|
||||
body {
|
||||
margin:0
|
||||
}
|
||||
button,input,select,textarea {
|
||||
font:inherit
|
||||
}
|
||||
button,a,input,select,textarea {
|
||||
outline-offset:4px
|
||||
}
|
||||
a {
|
||||
color:var(--accent);
|
||||
text-decoration:none
|
||||
}
|
||||
button,.button {
|
||||
cursor:pointer;
|
||||
border:1px solid var(--line);
|
||||
border-radius:9px;
|
||||
background:white;
|
||||
padding:10px 15px;
|
||||
color:#42495b;
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
white-space:nowrap;
|
||||
gap:6px
|
||||
}
|
||||
button:hover,.button:hover {
|
||||
background:#eeecfb;
|
||||
border-color:#c9c0ef
|
||||
}
|
||||
button:disabled {
|
||||
cursor:default;
|
||||
opacity:.45
|
||||
}
|
||||
button.primary,.button.primary {
|
||||
background:var(--accent);
|
||||
border-color:var(--accent);
|
||||
color:white
|
||||
}
|
||||
.primary:hover {
|
||||
background:#5745b8
|
||||
}
|
||||
.text-button {
|
||||
border:0;
|
||||
background:transparent;
|
||||
color:var(--accent);
|
||||
padding:8px 0
|
||||
}
|
||||
.danger-text {
|
||||
color:#b24558
|
||||
}
|
||||
.muted {
|
||||
color:var(--muted);
|
||||
font-size:13px;
|
||||
line-height:1.6;
|
||||
margin:7px 0
|
||||
}
|
||||
.eyebrow {
|
||||
font-size:10px;
|
||||
letter-spacing:1.7px;
|
||||
font-weight:700;
|
||||
color:var(--muted);
|
||||
margin:0 0 12px
|
||||
}
|
||||
h1 {
|
||||
font-size:32px;
|
||||
letter-spacing:-1px;
|
||||
margin:0 0 8px;
|
||||
font-weight:700
|
||||
}
|
||||
h2 {
|
||||
font-size:17px;
|
||||
margin:0 0 16px;
|
||||
letter-spacing:-.3px
|
||||
}
|
||||
label {
|
||||
display:block;
|
||||
font-size:12px;
|
||||
font-weight:600;
|
||||
color:#697184
|
||||
}
|
||||
input,select,textarea {
|
||||
display:block;
|
||||
width:100%;
|
||||
border:1px solid #dfe3ed;
|
||||
background:#fff;
|
||||
color:#202637;
|
||||
padding:10px 12px;
|
||||
border-radius:7px;
|
||||
font-size:14px;
|
||||
margin-top:7px
|
||||
}
|
||||
input:focus,select:focus,textarea:focus {
|
||||
border-color:var(--accent);
|
||||
outline:2px solid #e7e2ff
|
||||
}
|
||||
textarea {
|
||||
min-height:90px;
|
||||
resize:vertical
|
||||
}
|
||||
form>label {
|
||||
margin:18px 0
|
||||
}
|
||||
fieldset {
|
||||
min-width:0;
|
||||
border:0;
|
||||
padding:0;
|
||||
margin:0
|
||||
}
|
||||
input[type=checkbox] {
|
||||
width:18px;
|
||||
height:18px;
|
||||
accent-color:var(--accent)
|
||||
}
|
||||
.app-shell {
|
||||
display:flex;
|
||||
min-height:100vh
|
||||
}
|
||||
.sidebar {
|
||||
background:#1b2234;
|
||||
color:#eff0f7;
|
||||
width:235px;
|
||||
flex-shrink:0;
|
||||
padding:34px 20px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
position:sticky;
|
||||
top:0;
|
||||
height:100vh
|
||||
}
|
||||
.brand {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:13px;
|
||||
color:#fff;
|
||||
line-height:1.4;
|
||||
font-size:14px;
|
||||
padding:0 10px
|
||||
}
|
||||
.brand-mark {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:42px;
|
||||
height:42px;
|
||||
border-radius:12px;
|
||||
background:#8070e3;
|
||||
color:#fff;
|
||||
font-size:15px;
|
||||
font-weight:800;
|
||||
letter-spacing:-1px
|
||||
}
|
||||
.nav-label {
|
||||
font-size:9px;
|
||||
color:#8993ac;
|
||||
letter-spacing:1.2px;
|
||||
margin:50px 10px 16px
|
||||
}
|
||||
.sidebar nav {
|
||||
display:grid;
|
||||
gap:8px
|
||||
}
|
||||
.sidebar nav a {
|
||||
color:#aeb6c9;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
padding:14px 12px;
|
||||
border-radius:9px;
|
||||
font-size:13px
|
||||
}
|
||||
.sidebar nav a.active {
|
||||
background:#33314d;
|
||||
color:#ddd6ff
|
||||
}
|
||||
.sidebar nav a:hover {
|
||||
background:#283148
|
||||
}
|
||||
.sidebar-bottom {
|
||||
margin-top:auto;
|
||||
border-top:1px solid #31394c;
|
||||
padding:20px 10px 0
|
||||
}
|
||||
.sidebar-bottom .text-button {
|
||||
color:#c5bddf
|
||||
}
|
||||
.main-content {
|
||||
flex:1;
|
||||
min-width:0;
|
||||
padding:38px 42px;
|
||||
max-width:1750px
|
||||
}
|
||||
.page-heading {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:20px;
|
||||
margin:0 0 30px
|
||||
}
|
||||
.count-badge {
|
||||
background:#eeebfb;
|
||||
color:var(--accent);
|
||||
border-radius:30px;
|
||||
padding:9px 14px;
|
||||
font-size:12px;
|
||||
font-weight:600
|
||||
}
|
||||
.panel {
|
||||
border:1px solid var(--line);
|
||||
background:white;
|
||||
border-radius:13px
|
||||
}
|
||||
.compact {
|
||||
padding:23px 25px;
|
||||
margin-bottom:24px
|
||||
}
|
||||
.inline-form {
|
||||
display:flex;
|
||||
align-items:end;
|
||||
gap:16px
|
||||
}
|
||||
.grow {
|
||||
flex:1
|
||||
}
|
||||
.actions {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px
|
||||
}
|
||||
.client-grid {
|
||||
display:grid;
|
||||
grid-template-columns:repeat(auto-fill,minmax(280px,1fr));
|
||||
gap:18px;
|
||||
margin-top:25px
|
||||
}
|
||||
.client-card {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:17px;
|
||||
border:1px solid var(--line);
|
||||
background:#fff;
|
||||
border-radius:12px;
|
||||
padding:25px;
|
||||
color:inherit;
|
||||
transition:box-shadow .15s
|
||||
}
|
||||
.client-card:hover {
|
||||
box-shadow:0 6px 20px #22223b0a;
|
||||
border-color:#c8beed
|
||||
}
|
||||
.client-card h2 {
|
||||
margin:0;
|
||||
font-size:16px
|
||||
}
|
||||
.client-card .arrow {
|
||||
margin-left:auto;
|
||||
color:var(--accent)
|
||||
}
|
||||
.avatar {
|
||||
display:grid;
|
||||
place-items:center;
|
||||
background:#f0edff;
|
||||
color:var(--accent);
|
||||
font-size:20px;
|
||||
width:50px;
|
||||
height:50px;
|
||||
border-radius:15px
|
||||
}
|
||||
.empty-state {
|
||||
text-align:center;
|
||||
padding:70px 20px;
|
||||
border:1px dashed #d4d8e6;
|
||||
border-radius:14px;
|
||||
margin:24px 0;
|
||||
background:#ffffff80
|
||||
}
|
||||
.empty-symbol {
|
||||
display:block;
|
||||
font-size:35px;
|
||||
color:#9e91da;
|
||||
margin-bottom:20px
|
||||
}
|
||||
.empty-state h2 {
|
||||
margin-bottom:10px
|
||||
}
|
||||
.empty-state button {
|
||||
margin-top:18px
|
||||
}
|
||||
.breadcrumb {
|
||||
display:inline-block;
|
||||
color:var(--muted);
|
||||
font-size:12px;
|
||||
margin-bottom:24px
|
||||
}
|
||||
.section-title {
|
||||
margin:30px 0 18px
|
||||
}
|
||||
.program-row {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:20px;
|
||||
background:white;
|
||||
border:1px solid var(--line);
|
||||
border-radius:10px;
|
||||
padding:20px 24px;
|
||||
margin-bottom:12px
|
||||
}
|
||||
.program-title {
|
||||
font-size:17px;
|
||||
font-weight:650;
|
||||
color:#30384b
|
||||
}
|
||||
.program-row .muted {
|
||||
margin:8px 0 0;
|
||||
font-size:11px
|
||||
}
|
||||
summary {
|
||||
cursor:pointer;
|
||||
font-size:14px;
|
||||
font-weight:600
|
||||
}
|
||||
.form-grid {
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:20px;
|
||||
margin-bottom:20px
|
||||
}
|
||||
.list-toolbar {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
margin:25px 0 18px
|
||||
}
|
||||
.list-toolbar h2 {
|
||||
margin:0
|
||||
}
|
||||
.search {
|
||||
max-width:320px;
|
||||
margin:0
|
||||
}
|
||||
.title-input {
|
||||
font-size:28px;
|
||||
font-weight:700;
|
||||
background:transparent;
|
||||
border:0;
|
||||
padding:0;
|
||||
letter-spacing:-.7px;
|
||||
margin:0
|
||||
}
|
||||
.editor-heading {
|
||||
align-items:center;
|
||||
margin-bottom:30px
|
||||
}
|
||||
.editor-heading > div:first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.editor-heading > .actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.save-status {
|
||||
font-size:11px;
|
||||
color:#7b8497;
|
||||
max-width:130px
|
||||
}
|
||||
.week-tabs {
|
||||
display:flex;
|
||||
gap:4px;
|
||||
border-bottom:1px solid var(--line);
|
||||
margin-bottom:26px
|
||||
}
|
||||
.week-tabs button {
|
||||
display:block;
|
||||
background:transparent;
|
||||
border:0;
|
||||
border-bottom:3px solid transparent;
|
||||
border-radius:0;
|
||||
padding:16px 25px;
|
||||
text-align:left;
|
||||
font-size:14px
|
||||
}
|
||||
.week-tabs button span {
|
||||
display:block;
|
||||
font-size:10px;
|
||||
color:var(--muted);
|
||||
margin-top:7px;
|
||||
font-weight:400
|
||||
}
|
||||
.week-tabs button.selected {
|
||||
border-color:var(--accent);
|
||||
color:var(--accent);
|
||||
background:#eeebfa
|
||||
}
|
||||
.week-tabs button[draggable="true"] { cursor:grab; position:relative; }
|
||||
.week-tabs button[draggable="true"]:active { cursor:grabbing; }
|
||||
.week-tabs .week-dragging { opacity:.35; }
|
||||
.week-tabs .week-drop-before::before,
|
||||
.week-tabs .week-drop-after::after {
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:8px;
|
||||
bottom:8px;
|
||||
width:3px;
|
||||
border-radius:3px;
|
||||
background:var(--accent);
|
||||
box-shadow:0 0 0 3px #eeebfa;
|
||||
pointer-events:none
|
||||
}
|
||||
.week-tabs .week-drop-before::before { left:-4px; }
|
||||
.week-tabs .week-drop-after::after { right:-4px; }
|
||||
.week-drag-ghost {
|
||||
position:fixed;
|
||||
top:-1000px;
|
||||
left:0;
|
||||
width:72px;
|
||||
height:72px;
|
||||
box-sizing:border-box;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
padding:8px;
|
||||
border:2px dashed var(--accent);
|
||||
border-radius:8px;
|
||||
background:transparent;
|
||||
color:var(--accent);
|
||||
opacity:.75;
|
||||
font-family:inherit;
|
||||
font-size:14px;
|
||||
font-weight:600;
|
||||
pointer-events:none
|
||||
}
|
||||
.week-tabs .add-week {
|
||||
color:var(--accent);
|
||||
font-size:12px
|
||||
}
|
||||
.program-workspace {
|
||||
display:grid;
|
||||
grid-template-columns:185px minmax(0,1fr);
|
||||
gap:25px
|
||||
}
|
||||
.workout-nav {
|
||||
border-right:1px solid var(--line);
|
||||
padding-right:20px
|
||||
}
|
||||
.workout-nav .list-toolbar {
|
||||
margin:0 0 20px
|
||||
}
|
||||
.workout-nav h2 {
|
||||
font-size:13px
|
||||
}
|
||||
.icon-button {
|
||||
padding:3px 8px
|
||||
}
|
||||
.workout-tab {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:start;
|
||||
gap:10px;
|
||||
width:100%;
|
||||
margin-bottom:8px;
|
||||
text-align:left;
|
||||
white-space:normal;
|
||||
padding:14px 10px;
|
||||
background:transparent;
|
||||
border-color:transparent;
|
||||
font-size:12px
|
||||
}
|
||||
.workout-tab span {
|
||||
font-size:10px;
|
||||
color:#929bad
|
||||
}
|
||||
.workout-tab.selected {
|
||||
background:#eae6fb;
|
||||
border-color:#e1daf8;
|
||||
color:var(--accent)
|
||||
}
|
||||
.workout-tab[draggable="true"] { position:relative; cursor:grab; }
|
||||
.workout-tab[draggable="true"]:active { cursor:grabbing; }
|
||||
.workout-tab.workout-dragging { opacity:.35; }
|
||||
.workout-drop-before::before,
|
||||
.workout-drop-after::after {
|
||||
content:"";
|
||||
position:absolute;
|
||||
left:0;
|
||||
right:0;
|
||||
height:3px;
|
||||
border-radius:3px;
|
||||
background:var(--accent);
|
||||
box-shadow:0 0 0 3px #eeebfa;
|
||||
pointer-events:none
|
||||
}
|
||||
.workout-drop-before::before { top:-6px; }
|
||||
.workout-drop-after::after { bottom:-6px; }
|
||||
.workout-nav>.text-button {
|
||||
font-size:11px;
|
||||
margin-top:15px
|
||||
}
|
||||
.workout-heading {
|
||||
display:flex;
|
||||
gap:15px;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
margin-bottom:23px
|
||||
}
|
||||
.workout-heading input {
|
||||
font-size:20px;
|
||||
font-weight:650;
|
||||
background:transparent;
|
||||
border:0;
|
||||
padding:0;
|
||||
margin:0
|
||||
}
|
||||
.workout-heading button {
|
||||
font-size:11px
|
||||
}
|
||||
.block-card {
|
||||
background:white;
|
||||
border:1px solid var(--line);
|
||||
border-radius:12px;
|
||||
margin-bottom:18px;
|
||||
box-shadow:0 2px 5px #20253603;
|
||||
overflow:hidden
|
||||
}
|
||||
.block-header {
|
||||
padding:14px 18px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
border-bottom:1px solid #eeeff5
|
||||
}
|
||||
.block-header strong {
|
||||
font-size:12px;
|
||||
color:#626b7d
|
||||
}
|
||||
.block-header button {
|
||||
font-size:10px;
|
||||
padding:5px 8px;
|
||||
border-color:transparent
|
||||
}
|
||||
.block-number {
|
||||
font-size:11px;
|
||||
color:#a1a8b7
|
||||
}
|
||||
.drag-handle {
|
||||
cursor:grab;
|
||||
font-size:23px;
|
||||
color:#a5adbd;
|
||||
line-height:1
|
||||
}
|
||||
.exercise-columns {
|
||||
display:flex;
|
||||
gap:24px;
|
||||
padding:22px
|
||||
}
|
||||
.exercise-editor {
|
||||
flex:1;
|
||||
min-width:0
|
||||
}
|
||||
.exercise-heading {
|
||||
display:flex;
|
||||
align-items:end;
|
||||
gap:15px
|
||||
}
|
||||
.exercise-heading input {
|
||||
font-weight:600
|
||||
}
|
||||
.camera-toggle {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:5px;
|
||||
padding-bottom:9px;
|
||||
cursor:pointer
|
||||
}
|
||||
.camera-toggle input {
|
||||
margin:0
|
||||
}
|
||||
.camera-toggle span {
|
||||
font-size:18px
|
||||
}
|
||||
.video-input {
|
||||
margin:14px 0 20px;
|
||||
font-size:10px
|
||||
}
|
||||
.video-input input {
|
||||
font-size:12px;
|
||||
background:#fafbfe
|
||||
}
|
||||
.block-footer {
|
||||
background:#fafbfe;
|
||||
border-top:1px solid #eeeff5;
|
||||
padding:12px 20px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:15px
|
||||
}
|
||||
.block-footer button {
|
||||
font-size:10px;
|
||||
padding:6px 8px
|
||||
}
|
||||
.rest-input {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:9px;
|
||||
font-size:10px
|
||||
}
|
||||
.rest-input input {
|
||||
width:72px;
|
||||
margin:0;
|
||||
font-size:12px;
|
||||
padding:6px 8px
|
||||
}
|
||||
.add-exercise-bar {
|
||||
display:flex;
|
||||
align-items:end;
|
||||
gap:12px;
|
||||
padding:20px;
|
||||
border:1px dashed #cdd2e1;
|
||||
border-radius:11px;
|
||||
margin:24px 0
|
||||
}
|
||||
.add-exercise-bar label {
|
||||
font-size:10px
|
||||
}
|
||||
.add-exercise-bar button {
|
||||
font-size:11px
|
||||
}
|
||||
.error {
|
||||
background:#fff0f1;
|
||||
border:1px solid #f0cbd0;
|
||||
color:#a13d4c;
|
||||
padding:14px 18px;
|
||||
border-radius:9px;
|
||||
font-size:13px;
|
||||
margin:14px 0;
|
||||
line-height:1.5
|
||||
}
|
||||
.error:empty {
|
||||
display:none
|
||||
}
|
||||
.undo-message {
|
||||
font-size:12px;
|
||||
background:#eeebfb;
|
||||
padding:5px 16px;
|
||||
border-radius:8px;
|
||||
margin-bottom:16px
|
||||
}
|
||||
.login-page {
|
||||
min-height:100vh;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
background:#f0f1f7
|
||||
}
|
||||
.login-card {
|
||||
background:#fff;
|
||||
border:1px solid var(--line);
|
||||
border-radius:20px;
|
||||
padding:45px;
|
||||
width:430px;
|
||||
box-shadow:0 16px 70px #25305909
|
||||
}
|
||||
.login-card .brand-mark {
|
||||
margin-bottom:32px
|
||||
}
|
||||
.login-card h1 {
|
||||
font-size:28px
|
||||
}
|
||||
.login-card form {
|
||||
margin-top:28px
|
||||
}
|
||||
.login-card button {
|
||||
width:100%;
|
||||
margin-top:12px
|
||||
}
|
||||
.login-card .error ul {
|
||||
padding-left:15px
|
||||
}
|
||||
#blazor-error-ui {
|
||||
background:#fff1cc;
|
||||
bottom:0;
|
||||
box-shadow:0 -1px 2px #0003;
|
||||
display:none;
|
||||
left:0;
|
||||
padding:1rem;
|
||||
position:fixed;
|
||||
width:100%;
|
||||
z-index:1000
|
||||
}
|
||||
.validation-message {
|
||||
color:#a13d4c
|
||||
}
|
||||
@media(max-width:1200px) {
|
||||
.sidebar {
|
||||
width:200px;
|
||||
padding:30px 14px
|
||||
}
|
||||
.main-content {
|
||||
padding:30px 25px
|
||||
}
|
||||
.program-workspace {
|
||||
grid-template-columns:145px minmax(0,1fr);
|
||||
gap:18px
|
||||
}
|
||||
.week-tabs button {
|
||||
padding:15px 18px
|
||||
}
|
||||
.editor-heading {
|
||||
align-items:start;
|
||||
flex-direction:column
|
||||
}
|
||||
.block-footer {
|
||||
flex-wrap:wrap
|
||||
}
|
||||
.block-header {
|
||||
flex-wrap:wrap
|
||||
}
|
||||
.exercise-columns {
|
||||
gap:15px
|
||||
}
|
||||
}
|
||||
@media(max-width:760px) {
|
||||
.app-shell {
|
||||
min-width:1040px
|
||||
}
|
||||
.sidebar {
|
||||
width:190px
|
||||
}
|
||||
.main-content {
|
||||
padding:25px
|
||||
}
|
||||
.login-card {
|
||||
width:min(430px,calc(100vw - 32px));
|
||||
padding:30px
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-fields { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
|
||||
.exercise-picker { min-width:0; }
|
||||
.exercise-picker select { width:100%; }
|
||||
.group-tabs { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.group-tabs .selected { color:var(--accent); background:#eeebfa; border-color:var(--accent); }
|
||||
.group-actions,.group-form { margin-top:16px; }
|
||||
.group-form { max-width:440px; }
|
||||
.exercise-editor textarea { width:100%; resize:vertical; min-height:60px; }
|
||||
.workout-summary { display:flex; gap:24px; align-items:center; margin-bottom:20px; }
|
||||
.workout-summary label { max-width:240px; }
|
||||
.library-exercise[draggable="true"] { cursor:grab; }
|
||||
.library-dragging { opacity:.35; }
|
||||
.group-tabs .group-drop-target { outline:3px solid var(--accent); outline-offset:3px; background:#eeebfa; }
|
||||
.library-drag-ghost { font-size:10px; overflow:hidden; overflow-wrap:anywhere; text-align:center; }
|
||||
|
||||
.group-panel { position:sticky; top:0; z-index:5; }
|
||||
|
||||
.exercise-combobox { position:relative; }
|
||||
.exercise-combobox input { padding-right:44px; }
|
||||
.picker-toggle { position:absolute; right:2px; top:2px; bottom:2px; border:0; background:transparent; padding:0 12px; }
|
||||
.exercise-dropdown { position:fixed; inset:auto; margin:0; box-sizing:border-box; padding:6px; border:1px solid var(--line); border-radius:10px; background:white; box-shadow:0 10px 32px #20253626; overflow:auto; overscroll-behavior:contain; }
|
||||
.exercise-option { display:flex; flex-direction:column; align-items:flex-start; width:100%; padding:10px 12px; border:0; background:transparent; white-space:normal; text-align:left; overflow-wrap:anywhere; }
|
||||
.exercise-option strong { font-size:13px; }
|
||||
.exercise-option span { font-size:11px; color:var(--muted); }
|
||||
.exercise-option.active,.exercise-option[aria-selected="true"] { background:#eeebfa; color:var(--accent); }
|
||||
.picker-hint,.picker-selection { font-size:11px; color:var(--muted); margin:8px; }
|
||||
|
||||
.exercise-import { width:min(900px,calc(100vw - 32px)); max-height:calc(100dvh - 48px); overflow:auto; border:1px solid var(--line); border-radius:16px; padding:24px; color:var(--text); background:white; box-shadow:0 20px 80px #0003; }
|
||||
.exercise-import::backdrop { background:#14182780; }
|
||||
.exercise-import fieldset { min-width:0; }
|
||||
.import-table-scroll { overflow:auto; max-height:45vh; margin:18px 0; }
|
||||
.import-table { width:100%; border-collapse:collapse; table-layout:fixed; }
|
||||
.import-table th { text-align:left; background:#f5f4fa; position:sticky; top:0; z-index:1; padding:10px; }
|
||||
.import-table th:last-child { width:76px; }
|
||||
.import-table td { border:1px solid var(--line); padding:4px; }
|
||||
.import-table input { width:100%; min-width:0; border-color:transparent; }
|
||||
.exercise-import .actions { margin-top:20px; }
|
||||
@@ -0,0 +1,32 @@
|
||||
window.exerciseImport = {
|
||||
open(dialog, receiver, maxCharacters) {
|
||||
dialog.importPaste = async event => {
|
||||
const input = event.target.closest('input[data-row]');
|
||||
if (!input || input.matches(':disabled')) return;
|
||||
event.preventDefault();
|
||||
if (dialog.pasting) return;
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const error = dialog.querySelector('.clipboard-error');
|
||||
error.hidden = true;
|
||||
if (text.length > maxCharacters) {
|
||||
error.textContent = 'Слишком большой объём данных. Вставьте таблицу частями.';
|
||||
error.hidden = false;
|
||||
return;
|
||||
}
|
||||
dialog.pasting = true;
|
||||
try {
|
||||
await receiver.invokeMethodAsync('PasteAsync', text, Number(input.dataset.row), Number(input.dataset.column));
|
||||
} catch {
|
||||
error.textContent = 'Не удалось вставить данные. Проверьте соединение и повторите вставку.';
|
||||
error.hidden = false;
|
||||
} finally { dialog.pasting = false; }
|
||||
};
|
||||
dialog.addEventListener('paste', dialog.importPaste);
|
||||
dialog.showModal();
|
||||
dialog.querySelector('input[data-row]').focus();
|
||||
},
|
||||
close(dialog) {
|
||||
dialog.removeEventListener('paste', dialog.importPaste);
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
(() => {
|
||||
const popupFor = root => root.querySelector('.exercise-dropdown');
|
||||
function position(root) {
|
||||
const popup = popupFor(root);
|
||||
if (!popup?.matches(':popover-open')) return;
|
||||
const input = root.querySelector('.exercise-combobox');
|
||||
const bounds = input.getBoundingClientRect();
|
||||
const gap = 8;
|
||||
const below = window.innerHeight - bounds.bottom - gap;
|
||||
const above = bounds.top - gap;
|
||||
const upward = below < Math.min(popup.scrollHeight, 240) && above > below;
|
||||
const width = Math.min(bounds.width, window.innerWidth - gap * 2);
|
||||
popup.style.width = `${width}px`;
|
||||
popup.style.maxHeight = `${Math.max(0, Math.min(320, upward ? above : below) - gap)}px`;
|
||||
popup.style.left = `${Math.max(gap, Math.min(bounds.left, window.innerWidth - width - gap))}px`;
|
||||
popup.style.top = `${upward ? bounds.top - popup.getBoundingClientRect().height - gap : bounds.bottom + gap}px`;
|
||||
}
|
||||
window.trainingPlanner.positionExercisePicker = root => {
|
||||
position(root);
|
||||
const popup = popupFor(root);
|
||||
if (popup?.matches(':popover-open'))
|
||||
popup.querySelector('.active')?.scrollIntoView({ block: 'nearest' });
|
||||
};
|
||||
window.trainingPlanner.openExercisePicker = root => {
|
||||
if (root.querySelector('input[role="combobox"]').matches(':disabled')) return;
|
||||
popupFor(root).showPopover();
|
||||
position(root);
|
||||
};
|
||||
window.trainingPlanner.closeExercisePicker = root => popupFor(root).hidePopover();
|
||||
window.trainingPlanner.isExercisePickerOpen = root => popupFor(root).matches(':popover-open');
|
||||
document.addEventListener('toggle', event => {
|
||||
if (!event.target.matches('.exercise-dropdown')) return;
|
||||
const root = event.target.closest('.exercise-picker');
|
||||
root.querySelector('[role="combobox"]').setAttribute('aria-expanded', event.newState === 'open' ? 'true' : 'false');
|
||||
if (event.newState === 'open') position(root);
|
||||
}, true);
|
||||
document.addEventListener('keydown', event => {
|
||||
if (!event.target.matches('.exercise-picker input[role="combobox"]')) return;
|
||||
if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) event.preventDefault();
|
||||
if (event.key === 'Escape') popupFor(event.target.closest('.exercise-picker')).hidePopover();
|
||||
});
|
||||
function positionOpenPickers(event) {
|
||||
if (event?.target?.closest?.('.exercise-dropdown')) return;
|
||||
document.querySelectorAll('.exercise-dropdown:popover-open').forEach(popup => position(popup.closest('.exercise-picker')));
|
||||
}
|
||||
window.addEventListener('resize', positionOpenPickers);
|
||||
document.addEventListener('scroll', positionOpenPickers, true);
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect width="48" height="48" rx="12" fill="#8070e3"/><path d="M8 15h16v4h-6v17h-4V19H8zm19 0h8a7 7 0 0 1 0 14h-4v7h-4zm4 4v6h4a3 3 0 0 0 0-6z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 223 B |
@@ -0,0 +1,44 @@
|
||||
(() => {
|
||||
let source = null;
|
||||
let target = null;
|
||||
let ghost = null;
|
||||
function clearTarget() {
|
||||
target?.classList.remove('group-drop-target');
|
||||
target = null;
|
||||
}
|
||||
function clear() {
|
||||
clearTarget();
|
||||
source?.classList.remove('library-dragging');
|
||||
source = null;
|
||||
ghost?.remove();
|
||||
ghost = null;
|
||||
}
|
||||
document.addEventListener('dragstart', event => {
|
||||
const row = event.target.closest('.library-exercise[draggable="true"]');
|
||||
if (!row) return;
|
||||
clear();
|
||||
source = row;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', row.querySelector('strong').textContent);
|
||||
ghost = document.createElement('div');
|
||||
ghost.className = 'week-drag-ghost library-drag-ghost';
|
||||
ghost.textContent = row.querySelector('strong').textContent;
|
||||
document.body.appendChild(ghost);
|
||||
event.dataTransfer.setDragImage(ghost, ghost.offsetWidth + 12, ghost.offsetHeight + 12);
|
||||
requestAnimationFrame(() => source?.classList.add('library-dragging'));
|
||||
});
|
||||
document.addEventListener('dragover', event => {
|
||||
if (!source) return;
|
||||
clearTarget();
|
||||
target = event.target.closest('[data-exercise-group]');
|
||||
if (!target || target.matches(':disabled')) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
target.classList.add('group-drop-target');
|
||||
});
|
||||
document.addEventListener('dragleave', event => {
|
||||
if (!event.relatedTarget) clearTarget();
|
||||
});
|
||||
document.addEventListener('drop', clear);
|
||||
document.addEventListener('dragend', clear);
|
||||
})();
|
||||
@@ -0,0 +1,10 @@
|
||||
window.trainingPlanner = {
|
||||
download(url) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
(() => {
|
||||
const tabSelector = '.week-tabs [data-week-index]';
|
||||
let source = null;
|
||||
let marker = null;
|
||||
let ghost = null;
|
||||
|
||||
function clearMarker() {
|
||||
marker?.classList.remove('week-drop-before', 'week-drop-after');
|
||||
marker = null;
|
||||
}
|
||||
|
||||
function clearDrag() {
|
||||
clearMarker();
|
||||
source?.classList.remove('week-dragging');
|
||||
source = null;
|
||||
ghost?.remove();
|
||||
ghost = null;
|
||||
}
|
||||
|
||||
function isAfter(tab, clientX) {
|
||||
const bounds = tab.getBoundingClientRect();
|
||||
return clientX >= bounds.left + bounds.width / 2;
|
||||
}
|
||||
|
||||
window.trainingPlanner.weekInsertionIndex = (index, clientX) => {
|
||||
const tab = document.querySelector(`${tabSelector}[data-week-index="${index}"]`);
|
||||
return tab ? index + (isAfter(tab, clientX) ? 1 : 0) : -1;
|
||||
};
|
||||
|
||||
document.addEventListener('dragstart', event => {
|
||||
const tab = event.target.closest(tabSelector);
|
||||
if (!tab || tab.matches(':disabled')) return;
|
||||
clearDrag();
|
||||
source = tab;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', tab.dataset.weekIndex);
|
||||
ghost = document.createElement('div');
|
||||
ghost.className = 'week-drag-ghost';
|
||||
ghost.textContent = Array.from(tab.childNodes)
|
||||
.filter(node => node.nodeType === Node.TEXT_NODE)
|
||||
.map(node => node.textContent).join('').trim();
|
||||
document.body.appendChild(ghost);
|
||||
event.dataTransfer.setDragImage(ghost, ghost.offsetWidth + 12, ghost.offsetHeight + 12);
|
||||
requestAnimationFrame(() => source?.classList.add('week-dragging'));
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', event => {
|
||||
if (!source) return;
|
||||
const tab = event.target.closest(tabSelector);
|
||||
clearMarker();
|
||||
if (!tab || tab.parentElement !== source.parentElement) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
const after = isAfter(tab, event.clientX);
|
||||
const insertion = Number(tab.dataset.weekIndex) + (after ? 1 : 0);
|
||||
const origin = Number(source.dataset.weekIndex);
|
||||
if (insertion === origin || insertion === origin + 1) return;
|
||||
marker = tab;
|
||||
marker.classList.add(after ? 'week-drop-after' : 'week-drop-before');
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', event => {
|
||||
if (!event.relatedTarget) clearMarker();
|
||||
});
|
||||
document.addEventListener('drop', clearDrag);
|
||||
document.addEventListener('dragend', clearDrag);
|
||||
})();
|
||||
@@ -0,0 +1,65 @@
|
||||
(() => {
|
||||
const tabSelector = '.workout-nav [data-workout-index]';
|
||||
let source = null;
|
||||
let marker = null;
|
||||
let ghost = null;
|
||||
|
||||
function clearMarker() {
|
||||
marker?.classList.remove('workout-drop-before', 'workout-drop-after');
|
||||
marker = null;
|
||||
}
|
||||
|
||||
function clearDrag() {
|
||||
clearMarker();
|
||||
source?.classList.remove('workout-dragging');
|
||||
source = null;
|
||||
ghost?.remove();
|
||||
ghost = null;
|
||||
}
|
||||
|
||||
function isAfter(tab, clientY) {
|
||||
const bounds = tab.getBoundingClientRect();
|
||||
return clientY >= bounds.top + bounds.height / 2;
|
||||
}
|
||||
|
||||
window.trainingPlanner.workoutInsertionIndex = (index, clientY) => {
|
||||
const tab = document.querySelector(`${tabSelector}[data-workout-index="${index}"]`);
|
||||
return tab ? index + (isAfter(tab, clientY) ? 1 : 0) : -1;
|
||||
};
|
||||
|
||||
document.addEventListener('dragstart', event => {
|
||||
const tab = event.target.closest(tabSelector);
|
||||
if (!tab || tab.matches(':disabled')) return;
|
||||
clearDrag();
|
||||
source = tab;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', tab.dataset.workoutIndex);
|
||||
ghost = document.createElement('div');
|
||||
ghost.className = 'week-drag-ghost';
|
||||
ghost.textContent = tab.querySelector('span').textContent.trim();
|
||||
document.body.appendChild(ghost);
|
||||
event.dataTransfer.setDragImage(ghost, ghost.offsetWidth + 12, ghost.offsetHeight + 12);
|
||||
requestAnimationFrame(() => source?.classList.add('workout-dragging'));
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', event => {
|
||||
if (!source) return;
|
||||
const tab = event.target.closest(tabSelector);
|
||||
clearMarker();
|
||||
if (!tab || tab.parentElement !== source.parentElement) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
const after = isAfter(tab, event.clientY);
|
||||
const insertion = Number(tab.dataset.workoutIndex) + (after ? 1 : 0);
|
||||
const origin = Number(source.dataset.workoutIndex);
|
||||
if (insertion === origin || insertion === origin + 1) return;
|
||||
marker = tab;
|
||||
marker.classList.add(after ? 'workout-drop-after' : 'workout-drop-before');
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', event => {
|
||||
if (!event.relatedTarget) clearMarker();
|
||||
});
|
||||
document.addEventListener('drop', clearDrag);
|
||||
document.addEventListener('dragend', clearDrag);
|
||||
})();
|
||||
Reference in New Issue
Block a user