feat: бронирование товаров и сброс пароля по email
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<a href="/account?tab=profile" class="account-tabs__link <%= activeTab === 'profile' ? 'account-tabs__link--active' : '' %>">Профиль</a>
|
||||
<a href="/account?tab=email" class="account-tabs__link <%= activeTab === 'email' ? 'account-tabs__link--active' : '' %>">Смена email</a>
|
||||
<a href="/account?tab=password" class="account-tabs__link <%= activeTab === 'password' ? 'account-tabs__link--active' : '' %>">Смена пароля</a>
|
||||
<a href="/account?tab=reservations" class="account-tabs__link <%= activeTab === 'reservations' ? 'account-tabs__link--active' : '' %>">Бронирования</a>
|
||||
</nav>
|
||||
|
||||
<% if (activeTab === 'profile') { %>
|
||||
@@ -62,6 +63,45 @@
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (activeTab === 'reservations') { %>
|
||||
<section class="card account-section">
|
||||
<h2>Мои бронирования</h2>
|
||||
<% if (!reservations.length) { %>
|
||||
<p class="muted">Активных броней нет.</p>
|
||||
<% } else { %>
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Статус</th>
|
||||
<th>До</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% const resStatus = { active: 'Активна', fulfilled: 'Выполнена', cancelled: 'Отменена', expired: 'Истекла' }; %>
|
||||
<% reservations.forEach(r => { %>
|
||||
<tr>
|
||||
<td><a href="/product/<%= r.product_slug %>"><%= r.product_name %></a></td>
|
||||
<td><%= r.quantity %></td>
|
||||
<td><span class="status status--<%= r.status === 'active' ? 'pending' : r.status %>"><%= resStatus[r.status] || r.status %></span></td>
|
||||
<td><%= r.status === 'active' ? new Date(r.expires_at).toLocaleString('ru-RU') : '—' %></td>
|
||||
<td>
|
||||
<% if (r.status === 'active') { %>
|
||||
<form action="/reservations/<%= r.id %>/cancel" method="post" class="inline-form">
|
||||
<button type="submit" class="btn btn--ghost btn--sm">Отменить</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (activeTab === 'password') { %>
|
||||
<section class="card account-section account-section--narrow">
|
||||
<h2>Смена пароля</h2>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/orders" class="admin-nav__link">Заказы</a>
|
||||
<a href="/admin/users" class="admin-nav__link">Пользователи</a>
|
||||
<a href="/admin/products" class="admin-nav__link">Товары</a>
|
||||
<a href="/admin/reservations" class="admin-nav__link">Бронирования</a>
|
||||
<a href="/" class="admin-nav__link">В магазин</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/orders" class="admin-nav__link admin-nav__link--active">Заказы</a>
|
||||
<a href="/admin/users" class="admin-nav__link">Пользователи</a>
|
||||
<a href="/admin/products" class="admin-nav__link">Товары</a>
|
||||
<a href="/admin/reservations" class="admin-nav__link">Бронирования</a>
|
||||
<a href="/" class="admin-nav__link">В магазин</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/orders" class="admin-nav__link">Заказы</a>
|
||||
<a href="/admin/users" class="admin-nav__link">Пользователи</a>
|
||||
<a href="/admin/products" class="admin-nav__link admin-nav__link--active">Товары</a>
|
||||
<a href="/admin/reservations" class="admin-nav__link">Бронирования</a>
|
||||
<a href="/" class="admin-nav__link">В магазин</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<%- include('../partials/layout-start') %>
|
||||
|
||||
<div class="admin-header">
|
||||
<h1>Бронирования</h1>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin" class="admin-nav__link">Обзор</a>
|
||||
<a href="/admin/orders" class="admin-nav__link">Заказы</a>
|
||||
<a href="/admin/users" class="admin-nav__link">Пользователи</a>
|
||||
<a href="/admin/products" class="admin-nav__link">Товары</a>
|
||||
<a href="/admin/reservations" class="admin-nav__link admin-nav__link--active">Бронирования</a>
|
||||
<a href="/" class="admin-nav__link">В магазин</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<% const resStatus = { active: 'Активна', fulfilled: 'Выполнена', cancelled: 'Отменена', expired: 'Истекла' }; %>
|
||||
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>№</th>
|
||||
<th>Клиент</th>
|
||||
<th>Товар</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Статус</th>
|
||||
<th>До</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% reservations.forEach(r => { %>
|
||||
<tr>
|
||||
<td>#<%= r.id %></td>
|
||||
<td><%= r.user_name %><br><span class="muted"><%= r.user_email %></span></td>
|
||||
<td><%= r.product_name %></td>
|
||||
<td><%= r.quantity %></td>
|
||||
<td><span class="status status--<%= r.status === 'active' ? 'pending' : r.status %>"><%= resStatus[r.status] || r.status %></span></td>
|
||||
<td><%= r.status === 'active' ? new Date(r.expires_at).toLocaleString('ru-RU') : '—' %></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/reservations/<%= r.id %>/status" class="admin-status-form">
|
||||
<select name="status" class="input input--sm">
|
||||
<% ['active','fulfilled','cancelled','expired'].forEach(s => { %>
|
||||
<option value="<%= s %>" <%= r.status === s ? 'selected' : '' %>><%= resStatus[s] %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<button type="submit" class="btn btn--ghost btn--sm">OK</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%- include('../partials/layout-end') %>
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/orders" class="admin-nav__link">Заказы</a>
|
||||
<a href="/admin/users" class="admin-nav__link admin-nav__link--active">Пользователи</a>
|
||||
<a href="/admin/products" class="admin-nav__link">Товары</a>
|
||||
<a href="/admin/reservations" class="admin-nav__link">Бронирования</a>
|
||||
<a href="/" class="admin-nav__link">В магазин</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<%- include('../partials/layout-start') %>
|
||||
|
||||
<div class="auth">
|
||||
<form action="/forgot-password" method="post" class="form card">
|
||||
<h1>Сброс пароля</h1>
|
||||
<% if (error) { %><p class="alert alert--error"><%= error %></p><% } %>
|
||||
<% if (success) { %><p class="alert alert--success"><%= success %></p><% } %>
|
||||
<p class="muted">Укажите email аккаунта — отправим ссылку для нового пароля.</p>
|
||||
<label class="label">
|
||||
Email
|
||||
<input type="email" name="email" class="input" required value="<%= values.email || '' %>" autocomplete="email">
|
||||
</label>
|
||||
<button type="submit" class="btn btn--primary btn--block">Отправить ссылку</button>
|
||||
<p class="form-footer"><a href="/login">← Вход</a></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/layout-end') %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%- include('../partials/layout-start') %>
|
||||
|
||||
<div class="auth">
|
||||
<div class="card form">
|
||||
<h1>Пароль изменён</h1>
|
||||
<p class="alert alert--success">Теперь можно войти с новым паролем.</p>
|
||||
<a href="/login" class="btn btn--primary btn--block">Войти</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/layout-end') %>
|
||||
@@ -0,0 +1,28 @@
|
||||
<%- include('../partials/layout-start') %>
|
||||
|
||||
<div class="auth">
|
||||
<% if (token) { %>
|
||||
<form action="/reset-password" method="post" class="form card">
|
||||
<h1>Новый пароль</h1>
|
||||
<% if (error) { %><p class="alert alert--error"><%= error %></p><% } %>
|
||||
<input type="hidden" name="token" value="<%= token %>">
|
||||
<label class="label">
|
||||
Новый пароль
|
||||
<input type="password" name="password" class="input" required minlength="6" autocomplete="new-password">
|
||||
</label>
|
||||
<label class="label">
|
||||
Повторите пароль
|
||||
<input type="password" name="password2" class="input" required minlength="6" autocomplete="new-password">
|
||||
</label>
|
||||
<button type="submit" class="btn btn--primary btn--block">Сохранить пароль</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<div class="card form">
|
||||
<h1>Ссылка недействительна</h1>
|
||||
<% if (error) { %><p class="alert alert--error"><%= error %></p><% } %>
|
||||
<a href="/forgot-password" class="btn btn--primary">Запросить снова</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/layout-end') %>
|
||||
+4
-1
@@ -14,7 +14,10 @@
|
||||
<input type="password" name="password" class="input" required>
|
||||
</label>
|
||||
<button type="submit" class="btn btn--primary btn--block">Войти</button>
|
||||
<p class="form-footer">Нет аккаунта? <a href="/register">Регистрация</a></p>
|
||||
<p class="form-footer">
|
||||
<a href="/forgot-password">Забыли пароль?</a><br>
|
||||
Нет аккаунта? <a href="/register">Регистрация</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
<p class="product-detail__desc"><%= product.description %></p>
|
||||
<p class="product-detail__stock">В наличии: <strong><%= product.stock %></strong> шт.</p>
|
||||
|
||||
<% if (error) { %><p class="alert alert--error"><%= error %></p><% } %>
|
||||
<% if (reserved) { %><p class="alert alert--success">Товар успешно забронирован. Подробности на почте и в личном кабинете.</p><% } %>
|
||||
<% if (userReservation) { %>
|
||||
<p class="alert alert--success">
|
||||
У вас активная бронь: <%= userReservation.quantity %> шт. до <%= new Date(userReservation.expires_at).toLocaleString('ru-RU') %>.
|
||||
<a href="/account?tab=reservations">Мои бронирования</a>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<% if (product.stock > 0) { %>
|
||||
<form action="/cart/add" method="post" class="product-detail__form">
|
||||
<input type="hidden" name="product_id" value="<%= product.id %>">
|
||||
@@ -27,6 +36,20 @@
|
||||
<input type="hidden" name="redirect" value="/cart">
|
||||
<button type="submit" class="btn btn--primary btn--lg">Добавить в корзину</button>
|
||||
</form>
|
||||
|
||||
<% if (user && !userReservation) { %>
|
||||
<form action="/reservations" method="post" class="product-detail__form">
|
||||
<input type="hidden" name="product_id" value="<%= product.id %>">
|
||||
<input type="hidden" name="slug" value="<%= product.slug %>">
|
||||
<label class="label">
|
||||
Бронь (48 ч)
|
||||
<input type="number" name="quantity" value="1" min="1" max="<%= product.stock %>" class="input input--qty">
|
||||
</label>
|
||||
<button type="submit" class="btn btn--ghost btn--lg">Забронировать</button>
|
||||
</form>
|
||||
<% } else if (!user) { %>
|
||||
<p class="muted">Для бронирования <a href="/login">войдите</a> в аккаунт.</p>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<p class="alert alert--warn">Нет в наличии</p>
|
||||
<% } %>
|
||||
|
||||
Reference in New Issue
Block a user