initial commit

This commit is contained in:
commit 6f573999ef
35 changed files with 4598 additions and 0 deletions

696
app.py Normal file
View file

@ -0,0 +1,696 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
import sqlite3
import os
import re
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
from datetime import datetime
app = Flask(__name__)
app.secret_key = 'your_secret_key_here_change_in_production'
# --------------------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---------------------
def get_db_connection():
conn = sqlite3.connect('horse_farm.db')
conn.row_factory = sqlite3.Row
return conn
def is_valid_email(email):
if not email:
return True
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.(ru|com)$'
return re.match(pattern, email.lower()) is not None
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('Пожалуйста, войдите в систему', 'warning')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('Пожалуйста, войдите в систему', 'warning')
return redirect(url_for('login'))
if session.get('role') != 'admin':
flash('Доступ запрещен. Требуются права администратора.', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
# ------------------------- API -------------------------
@app.route('/api/services')
def api_services():
conn = get_db_connection()
services = conn.execute('SELECT id, name, price, duration FROM services').fetchall()
abonements = conn.execute('SELECT id, name, price, description FROM abonements').fetchall()
certificates = conn.execute('SELECT id, name, price, description FROM certificates').fetchall()
conn.close()
return jsonify({
'services': [dict(row) for row in services],
'abonements': [dict(row) for row in abonements],
'certificates': [dict(row) for row in certificates]
})
@app.route('/api/unread_count')
@login_required
def api_unread_count():
conn = get_db_connection()
count = conn.execute('''
SELECT COUNT(*) as count FROM messages m
JOIN orders o ON m.order_id = o.id
WHERE o.user_id = ? AND m.is_read = 0 AND m.sender_type != 'user'
''', (session['user_id'],)).fetchone()
conn.close()
return jsonify({'count': count['count'] if count else 0})
@app.route('/api/unread_by_order')
@login_required
def api_unread_by_order():
conn = get_db_connection()
unread = conn.execute('''
SELECT o.id, COUNT(*) as count
FROM messages m
JOIN orders o ON m.order_id = o.id
WHERE o.user_id = ? AND m.is_read = 0 AND m.sender_type != 'user'
GROUP BY o.id
''', (session['user_id'],)).fetchall()
conn.close()
return jsonify([dict(row) for row in unread])
@app.route('/api/mark_read', methods=['POST'])
@login_required
def mark_read():
data = request.get_json()
order_id = data.get('order_id')
conn = get_db_connection()
conn.execute('''
UPDATE messages SET is_read = 1
WHERE order_id = ? AND user_id = ? AND sender_type != 'user'
''', (order_id, session['user_id']))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/set_timezone', methods=['POST'])
def set_timezone():
data = request.get_json()
if data and 'timezone_offset' in data:
session['timezone_offset'] = data['timezone_offset']
return jsonify({'success': True})
return jsonify({'success': False}), 400
@app.route('/api/admin_unread_counts')
@admin_required
def api_admin_unread_counts():
conn = get_db_connection()
counts = conn.execute('''
SELECT order_id, COUNT(*) as count
FROM messages
WHERE is_read = 0 AND sender_type = 'user'
GROUP BY order_id
''').fetchall()
conn.close()
return jsonify({str(row['order_id']): row['count'] for row in counts})
@app.route('/api/mark_order_read', methods=['POST'])
@admin_required
def mark_order_read():
data = request.get_json()
order_id = data.get('order_id')
conn = get_db_connection()
conn.execute('UPDATE messages SET is_read = 1 WHERE order_id = ? AND sender_type = "user"', (order_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/toggle_payment', methods=['POST'])
@admin_required
def toggle_payment():
data = request.get_json()
order_id = data.get('order_id')
conn = get_db_connection()
current = conn.execute('SELECT payment_status FROM orders WHERE id = ?', (order_id,)).fetchone()
new_status = 'paid' if current['payment_status'] != 'paid' else 'not_paid'
conn.execute('UPDATE orders SET payment_status = ? WHERE id = ?', (new_status, order_id))
conn.commit()
conn.close()
return jsonify({'status': new_status})
# ---------------------- СТРАНИЦЫ ----------------------
@app.route('/')
def index():
return render_template('index.html')
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/services')
def services():
conn = get_db_connection()
services = conn.execute('SELECT * FROM services').fetchall()
conn.close()
return render_template('services.html', services=services)
@app.route('/abonements')
def abonements():
conn = get_db_connection()
abonements = conn.execute('SELECT * FROM abonements').fetchall()
conn.close()
return render_template('abonements.html', abonements=abonements)
@app.route('/certificates')
def certificates():
conn = get_db_connection()
certificates = conn.execute('SELECT * FROM certificates').fetchall()
conn.close()
return render_template('certificates.html', certificates=certificates)
@app.route('/gallery')
def gallery():
images_dir = os.path.join(app.static_folder, 'images/gallery')
images = []
if os.path.exists(images_dir):
images = [f for f in os.listdir(images_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp'))]
return render_template('gallery.html', images=images)
@app.route('/discounts')
def discounts():
return render_template('discounts.html')
# ---------------------- РЕГИСТРАЦИЯ / ВХОД ----------------------
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
email = request.form.get('email', '')
if email and not is_valid_email(email):
flash('Введите корректный email адрес (должен заканчиваться на .ru или .com)', 'danger')
return redirect(url_for('register'))
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
if user:
flash('Пользователь с таким именем уже существует', 'danger')
conn.close()
return redirect(url_for('register'))
hashed_password = generate_password_hash(password)
conn.execute('INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)',
(username, hashed_password, email if email else None, 'user'))
conn.commit()
conn.close()
flash('Регистрация успешна! Теперь вы можете войти', 'success')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
conn.close()
if user and check_password_hash(user['password'], password):
session.permanent = True
session['user_id'] = user['id']
session['username'] = user['username']
session['role'] = user['role']
flash(f'Добро пожаловать, {username}!', 'success')
return redirect(url_for('index'))
else:
flash('Неверное имя пользователя или пароль', 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
flash('Вы вышли из системы', 'info')
return redirect(url_for('index'))
# ---------------------- ЗАКАЗЫ ----------------------
@app.route('/order', methods=['GET', 'POST'])
@login_required
def order():
if request.method == 'POST':
service_type = request.form['service_type']
service_id = request.form.get('service_id')
service_name = request.form['service_name']
date = request.form.get('date')
time = request.form.get('time')
persons = request.form.get('persons', 1)
price = request.form.get('price', 0)
comment = request.form.get('comment', '')
try:
persons = int(float(persons)) if service_type != 'certificate' else 1
if persons < 1: persons = 1
if persons > 20: persons = 20
except:
persons = 1
conn = get_db_connection()
final_price = int(price)
discount_applied = False
discount_amount = 0
original_price = int(price)
if service_type != 'certificate':
duration_minutes = 0
duration_str = ''
if service_type == 'service':
service = conn.execute('SELECT duration FROM services WHERE id = ?', (service_id,)).fetchone()
if service:
duration_str = service['duration']
if 'минут' in duration_str:
duration_minutes = int(duration_str.split()[0])
elif 'час' in duration_str:
hours = int(duration_str.split()[0])
duration_minutes = hours * 60
try:
hour = int(time.split(':')[0])
minute = int(time.split(':')[1])
start_minutes = hour * 60 + minute
end_minutes = start_minutes + duration_minutes
if start_minutes < 540:
flash('Заказы принимаются с 9:00', 'danger')
conn.close()
return redirect(url_for('order'))
if end_minutes > 1200:
max_hour = (1200 - duration_minutes) // 60
max_minute = (1200 - duration_minutes) % 60
flash(f'Услуга длится {duration_str}. Необходимо закончить до 20:00. Максимальное время начала: {max_hour}:{max_minute:02d}', 'danger')
conn.close()
return redirect(url_for('order'))
except:
flash('Неверный формат времени', 'danger')
conn.close()
return redirect(url_for('order'))
today = datetime.now().strftime('%Y-%m-%d')
if date < today:
flash('Нельзя выбрать прошедшую дату', 'danger')
conn.close()
return redirect(url_for('order'))
if persons >= 4 and service_type == 'service':
discount_amount = int(int(price) * 0.15)
final_price = int(int(price) * 0.85)
discount_applied = True
conn.execute('''
INSERT INTO orders
(user_id, service_type, service_name, service_id, date, time, persons, price, comment, discount_applied, discount_amount, original_price, payment_status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'not_paid')
''', (session['user_id'], service_type, service_name, service_id, date, time, persons, final_price, comment, discount_applied, discount_amount, original_price))
conn.commit()
conn.close()
if discount_applied:
flash(f'Заказ успешно создан! Применена семейная скидка 15%! Итоговая цена: {final_price} руб.', 'success')
else:
flash('Заказ успешно создан!', 'success')
return redirect(url_for('my_orders'))
return render_template('order_form.html')
@app.route('/my-orders')
@login_required
def my_orders():
sort = request.args.get('sort', 'newest')
status = request.args.get('status', 'all')
conn = get_db_connection()
query = 'SELECT * FROM orders WHERE user_id = ?'
params = [session['user_id']]
if status != 'all':
query += ' AND status = ?'
params.append(status)
if sort == 'newest':
query += ' ORDER BY created_at DESC'
elif sort == 'oldest':
query += ' ORDER BY created_at ASC'
elif sort == 'price_high':
query += ' ORDER BY price DESC'
elif sort == 'price_low':
query += ' ORDER BY price ASC'
orders = conn.execute(query, params).fetchall()
orders_list = []
for order in orders:
order_dict = dict(order)
unread = conn.execute('''
SELECT COUNT(*) as count FROM messages
WHERE order_id = ? AND is_read = 0 AND sender_type != 'user'
''', (order['id'],)).fetchone()
order_dict['unread_count'] = unread['count'] if unread else 0
orders_list.append(order_dict)
conn.close()
return render_template('my_orders.html', orders=orders_list, sort=sort, status=status)
# ---------------------- ОТЗЫВЫ ----------------------
@app.route('/reviews', methods=['GET', 'POST'])
def reviews():
if request.method == 'POST':
if 'user_id' not in session:
flash('Пожалуйста, войдите в систему, чтобы оставить отзыв', 'warning')
return redirect(url_for('login'))
order_id = request.form.get('order_id')
rating = int(request.form['rating'])
comment = request.form['comment']
conn = get_db_connection()
existing_review = conn.execute('SELECT * FROM reviews WHERE user_id = ? AND order_id = ?',
(session['user_id'], order_id)).fetchone()
if existing_review:
flash('Вы уже оставляли отзыв на этот заказ', 'danger')
conn.close()
return redirect(url_for('reviews'))
order = conn.execute('SELECT * FROM orders WHERE id = ? AND user_id = ? AND status = "completed"',
(order_id, session['user_id'])).fetchone()
if not order:
flash('Вы можете оставить отзыв только на выполненные заказы', 'danger')
conn.close()
return redirect(url_for('reviews'))
conn.execute('''
INSERT INTO reviews (user_id, order_id, username, rating, comment, status)
VALUES (?, ?, ?, ?, ?, 'pending')
''', (session['user_id'], order_id, session['username'], rating, comment))
conn.commit()
conn.close()
flash('Ваш отзыв отправлен на модерацию!', 'success')
return redirect(url_for('reviews'))
sort = request.args.get('sort', 'newest')
rating_filter = request.args.get('rating', 'all')
conn = get_db_connection()
completed_orders = []
if 'user_id' in session:
completed_orders = conn.execute('''
SELECT o.* FROM orders o
LEFT JOIN reviews r ON o.id = r.order_id AND r.user_id = ?
WHERE o.user_id = ? AND o.status = 'completed' AND r.id IS NULL
ORDER BY o.created_at DESC
''', (session['user_id'], session['user_id'])).fetchall()
query = 'SELECT * FROM reviews WHERE status = "approved"'
params = []
if rating_filter != 'all':
query += ' AND rating = ?'
params.append(rating_filter)
if sort == 'newest':
query += ' ORDER BY created_at DESC'
elif sort == 'oldest':
query += ' ORDER BY created_at ASC'
elif sort == 'rating_high':
query += ' ORDER BY rating DESC'
elif sort == 'rating_low':
query += ' ORDER BY rating ASC'
approved_reviews = conn.execute(query, params).fetchall()
conn.close()
return render_template('reviews.html', reviews=approved_reviews,
completed_orders=completed_orders,
sort=sort, rating_filter=rating_filter)
# ---------------------- ЧАТ ----------------------
@app.route('/chat/<int:order_id>')
@login_required
def chat(order_id):
conn = get_db_connection()
order = conn.execute('SELECT * FROM orders WHERE id = ?', (order_id,)).fetchone()
if not order:
flash('Заказ не найден', 'danger')
return redirect(url_for('my_orders'))
if session['user_id'] != order['user_id'] and session.get('role') != 'admin':
flash('Доступ запрещен', 'danger')
return redirect(url_for('index'))
if session.get('role') == 'admin':
conn.execute('UPDATE messages SET is_read = 1 WHERE order_id = ? AND sender_type = "user"', (order_id,))
messages = conn.execute('''
SELECT m.*,
CASE
WHEN m.sender_type = 'user' THEN u.username
WHEN m.sender_type = 'admin' THEN a.username
ELSE 'Система'
END as sender_name
FROM messages m
LEFT JOIN users u ON m.user_id = u.id
LEFT JOIN users a ON m.admin_id = a.id
WHERE m.order_id = ?
ORDER BY m.created_at ASC
''', (order_id,)).fetchall()
conn.close()
return render_template('chat.html', order=order, messages=messages)
@app.route('/send_message', methods=['POST'])
@login_required
def send_message():
order_id = request.form['order_id']
message = request.form['message']
if not message or not message.strip():
return jsonify({'error': 'Сообщение не может быть пустым'}), 400
conn = get_db_connection()
order = conn.execute('SELECT * FROM orders WHERE id = ?', (order_id,)).fetchone()
if not order:
conn.close()
return jsonify({'error': 'Заказ не найден'}), 404
if session['user_id'] != order['user_id'] and session.get('role') != 'admin':
conn.close()
return jsonify({'error': 'Доступ запрещен'}), 403
if session.get('role') == 'admin':
sender_type = 'admin'
admin_id = session['user_id']
user_id = order['user_id']
else:
sender_type = 'user'
user_id = session['user_id']
admin_id = None
conn.execute('''
INSERT INTO messages (order_id, user_id, admin_id, message, sender_type)
VALUES (?, ?, ?, ?, ?)
''', (order_id, user_id, admin_id, message.strip(), sender_type))
conn.commit()
conn.close()
return jsonify({'success': True, 'message': 'Сообщение отправлено'})
# ---------------------- АДМИНКА ----------------------
@app.route('/admin')
@admin_required
def admin_panel():
return render_template('admin/index.html')
@app.route('/admin/orders')
@admin_required
def admin_orders():
sort = request.args.get('sort', 'newest')
status = request.args.get('status', 'all')
conn = get_db_connection()
query = '''
SELECT o.*, u.username,
(SELECT COUNT(*) FROM messages m WHERE m.order_id = o.id AND m.is_read = 0 AND m.sender_type != 'admin') as unread_count
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE 1=1
'''
params = []
if status != 'all':
query += ' AND o.status = ?'
params.append(status)
if sort == 'newest':
query += ' ORDER BY o.created_at DESC'
elif sort == 'oldest':
query += ' ORDER BY o.created_at ASC'
elif sort == 'price_high':
query += ' ORDER BY o.price DESC'
elif sort == 'price_low':
query += ' ORDER BY o.price ASC'
orders = conn.execute(query, params).fetchall()
conn.close()
return render_template('admin/orders.html', orders=orders, sort=sort, status=status)
@app.route('/admin/orders/<int:order_id>/update', methods=['POST'])
@admin_required
def update_order(order_id):
status = request.form['status']
sort = request.args.get('sort', 'newest')
status_filter = request.args.get('status', 'all')
conn = get_db_connection()
order = conn.execute('SELECT user_id FROM orders WHERE id = ?', (order_id,)).fetchone()
conn.execute('UPDATE orders SET status = ? WHERE id = ?', (status, order_id))
if order:
conn.execute('''
INSERT INTO messages (order_id, user_id, admin_id, message, sender_type)
VALUES (?, ?, ?, ?, 'system')
''', (order_id, order['user_id'], session['user_id'], f'Статус заказа изменен на: {status}'))
conn.commit()
conn.close()
flash('Статус заказа обновлен', 'success')
return redirect(url_for('admin_orders', sort=sort, status=status_filter))
@app.route('/admin/orders/<int:order_id>/delete', methods=['POST'])
@admin_required
def delete_order(order_id):
sort = request.args.get('sort', 'newest')
status_filter = request.args.get('status', 'all')
conn = get_db_connection()
conn.execute('DELETE FROM messages WHERE order_id = ?', (order_id,))
conn.execute('DELETE FROM orders WHERE id = ?', (order_id,))
conn.commit()
conn.close()
flash('Заказ удален', 'success')
return redirect(url_for('admin_orders', sort=sort, status=status_filter))
@app.route('/admin/reviews')
@admin_required
def admin_reviews():
sort = request.args.get('sort', 'newest')
status = request.args.get('status', 'all')
rating = request.args.get('rating', 'all')
conn = get_db_connection()
query = 'SELECT * FROM reviews WHERE 1=1'
params = []
if status != 'all':
query += ' AND status = ?'
params.append(status)
if rating != 'all':
query += ' AND rating = ?'
params.append(rating)
if sort == 'newest':
query += ' ORDER BY created_at DESC'
elif sort == 'oldest':
query += ' ORDER BY created_at ASC'
elif sort == 'rating_high':
query += ' ORDER BY rating DESC'
elif sort == 'rating_low':
query += ' ORDER BY rating ASC'
reviews = conn.execute(query, params).fetchall()
conn.close()
return render_template('admin/reviews.html', reviews=reviews, sort=sort, status=status, rating=rating)
@app.route('/admin/reviews/<int:review_id>/moderate', methods=['POST'])
@admin_required
def moderate_review(review_id):
status = request.form['status']
sort = request.args.get('sort', 'newest')
status_filter = request.args.get('status', 'all')
rating_filter = request.args.get('rating', 'all')
conn = get_db_connection()
conn.execute('UPDATE reviews SET status = ? WHERE id = ?', (status, review_id))
conn.commit()
conn.close()
flash('Отзыв обработан', 'success')
return redirect(url_for('admin_reviews', sort=sort, status=status_filter, rating=rating_filter))
@app.route('/admin/reviews/<int:review_id>/delete', methods=['POST'])
@admin_required
def delete_review(review_id):
sort = request.args.get('sort', 'newest')
status_filter = request.args.get('status', 'all')
rating_filter = request.args.get('rating', 'all')
conn = get_db_connection()
conn.execute('DELETE FROM reviews WHERE id = ?', (review_id,))
conn.commit()
conn.close()
flash('Отзыв удален', 'success')
return redirect(url_for('admin_reviews', sort=sort, status=status_filter, rating=rating_filter))
@app.route('/admin/users')
@admin_required
def admin_users():
sort = request.args.get('sort', 'newest')
role = request.args.get('role', 'all')
conn = get_db_connection()
query = 'SELECT * FROM users WHERE 1=1'
params = []
if role != 'all':
query += ' AND role = ?'
params.append(role)
if sort == 'newest':
query += ' ORDER BY created_at DESC'
elif sort == 'oldest':
query += ' ORDER BY created_at ASC'
elif sort == 'name_asc':
query += ' ORDER BY username ASC'
elif sort == 'name_desc':
query += ' ORDER BY username DESC'
users = conn.execute(query, params).fetchall()
conn.close()
return render_template('admin/users.html', users=users, sort=sort, role=role)
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
@admin_required
def delete_user(user_id):
sort = request.args.get('sort', 'newest')
role_filter = request.args.get('role', 'all')
if user_id == session['user_id']:
flash('Нельзя удалить самого себя', 'danger')
return redirect(url_for('admin_users', sort=sort, role=role_filter))
conn = get_db_connection()
conn.execute('DELETE FROM messages WHERE user_id = ?', (user_id,))
conn.execute('DELETE FROM orders WHERE user_id = ?', (user_id,))
conn.execute('DELETE FROM reviews WHERE user_id = ?', (user_id,))
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
conn.close()
flash('Пользователь удален', 'success')
return redirect(url_for('admin_users', sort=sort, role=role_filter))
@app.route('/admin/users/<int:user_id>/make-admin', methods=['POST'])
@admin_required
def make_admin(user_id):
sort = request.args.get('sort', 'newest')
role_filter = request.args.get('role', 'all')
conn = get_db_connection()
conn.execute('UPDATE users SET role = "admin" WHERE id = ?', (user_id,))
conn.commit()
conn.close()
flash('Пользователь назначен администратором', 'success')
return redirect(url_for('admin_users', sort=sort, role=role_filter))
@app.route('/admin/messages')
@admin_required
def admin_messages():
conn = get_db_connection()
messages = conn.execute('''
SELECT m.*, o.id as order_id, u.username as user_name
FROM messages m
JOIN orders o ON m.order_id = o.id
JOIN users u ON o.user_id = u.id
WHERE m.sender_type = 'user' AND m.is_read = 0
ORDER BY m.created_at DESC
''').fetchall()
conn.close()
return render_template('admin/messages.html', messages=messages)
# ---------------------- ЗАПУСК ----------------------
if __name__ == '__main__':
conn = sqlite3.connect('horse_farm.db')
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(orders)")
existing = [col[1] for col in cursor.fetchall()]
for col in ['discount_applied', 'discount_amount', 'original_price', 'payment_status']:
if col not in existing:
cursor.execute(f"ALTER TABLE orders ADD COLUMN {col} DEFAULT 'not_paid' if col == 'payment_status' else 0")
print(f"Добавлена колонка {col}")
cursor.execute("PRAGMA table_info(reviews)")
if 'order_id' not in [c[1] for c in cursor.fetchall()]:
cursor.execute("ALTER TABLE reviews ADD COLUMN order_id INTEGER")
cursor.execute("PRAGMA table_info(messages)")
if 'is_read' not in [c[1] for c in cursor.fetchall()]:
cursor.execute("ALTER TABLE messages ADD COLUMN is_read BOOLEAN DEFAULT 0")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'")
if not cursor.fetchone():
cursor.execute('''
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
user_id INTEGER,
admin_id INTEGER,
message TEXT NOT NULL,
sender_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_read BOOLEAN DEFAULT 0
)
''')
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone():
hashed = generate_password_hash('admin123')
cursor.execute("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
('admin', hashed, 'admin@koniydvor.ru', 'admin'))
print("Создан администратор: admin / admin123")
conn.commit()
conn.close()
app.run(debug=True)

108
create_tables.py Normal file
View file

@ -0,0 +1,108 @@
import sqlite3
def create_tables():
conn = sqlite3.connect('horse_farm.db')
cursor = conn.cursor()
# Таблица для услуг
cursor.execute('''
CREATE TABLE IF NOT EXISTS services (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price INTEGER,
duration TEXT
)
''')
# Таблица для абонементов
cursor.execute('''
CREATE TABLE IF NOT EXISTS abonements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price INTEGER,
description TEXT
)
''')
# Таблица для подарочных сертификатов
cursor.execute('''
CREATE TABLE IF NOT EXISTS certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price INTEGER,
description TEXT
)
''')
# Таблица для пользователей
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
role TEXT DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Таблица для заказов
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
service_type TEXT NOT NULL,
service_name TEXT NOT NULL,
service_id INTEGER,
date TEXT NOT NULL,
time TEXT NOT NULL,
persons INTEGER DEFAULT 1,
price INTEGER,
status TEXT DEFAULT 'pending',
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Таблица для отзывов (добавляем поле order_id)
cursor.execute('''
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
order_id INTEGER,
username TEXT,
rating INTEGER CHECK(rating >= 1 AND rating <= 5),
comment TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (order_id) REFERENCES orders (id),
UNIQUE(user_id, order_id)
)
''')
# Таблица для сообщений
cursor.execute('''
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
user_id INTEGER,
admin_id INTEGER,
message TEXT NOT NULL,
sender_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_read BOOLEAN DEFAULT 0,
FOREIGN KEY (order_id) REFERENCES orders (id),
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (admin_id) REFERENCES users (id)
)
''')
conn.commit()
conn.close()
print("Таблицы успешно созданы!")
if __name__ == '__main__':
create_tables()

BIN
horse_farm.db Normal file

Binary file not shown.

134
load_date.py Normal file
View file

@ -0,0 +1,134 @@
import sqlite3
import pandas as pd
import os
import re
def extract_price(price_str):
"""Извлекает число из строки с ценой"""
if pd.isna(price_str):
return 0
# Ищем число в строке (может быть "500 руб." или "500")
numbers = re.findall(r'\d+', str(price_str))
return int(numbers[0]) if numbers else 0
def find_column(columns, possible_names):
"""Ищет колонку по возможным названиям"""
for col in columns:
col_lower = col.lower()
for name in possible_names:
if name in col_lower:
return col
return columns[0] # возвращаем первую колонку если ничего не найдено
def load_from_excel_to_db():
"""Загрузка данных из Excel файлов в базу данных"""
# Подключаемся к БД
conn = sqlite3.connect('horse_farm.db')
cursor = conn.cursor()
# Очищаем таблицы перед загрузкой
cursor.execute("DELETE FROM services")
cursor.execute("DELETE FROM abonements")
cursor.execute("DELETE FROM certificates")
# Сбрасываем автоинкремент
cursor.execute("DELETE FROM sqlite_sequence WHERE name='services'")
cursor.execute("DELETE FROM sqlite_sequence WHERE name='abonements'")
cursor.execute("DELETE FROM sqlite_sequence WHERE name='certificates'")
# Загрузка услуг из Excel
services_file = 'resource/services.xlsx'
if os.path.exists(services_file):
df_services = pd.read_excel(services_file)
print(f"Загружаем услуги из {services_file}")
print(f"Колонки: {list(df_services.columns)}")
# Определяем колонки
name_col = find_column(df_services.columns, ['name', 'название', 'имя'])
desc_col = find_column(df_services.columns, ['desc', 'опис', 'описание'])
price_col = find_column(df_services.columns, ['price', 'цен', 'стоим'])
duration_col = find_column(df_services.columns, ['duration', 'длит', 'врем'])
print(f"Используем колонки: name='{name_col}', description='{desc_col}', price='{price_col}', duration='{duration_col}'")
for _, row in df_services.iterrows():
price = extract_price(row[price_col])
cursor.execute('''
INSERT INTO services (name, description, price, duration)
VALUES (?, ?, ?, ?)
''', (
str(row[name_col]),
str(row[desc_col]) if pd.notna(row[desc_col]) else '',
price,
str(row[duration_col]) if pd.notna(row[duration_col]) else ''
))
print(f"Загружено {len(df_services)} услуг")
else:
print(f"Файл {services_file} не найден")
# Загрузка абонементов из Excel
abonements_file = 'resource/abonements.xlsx'
if os.path.exists(abonements_file):
df_abonements = pd.read_excel(abonements_file)
print(f"\nЗагружаем абонементы из {abonements_file}")
print(f"Колонки: {list(df_abonements.columns)}")
# Определяем колонки
name_col = find_column(df_abonements.columns, ['name', 'название', 'имя'])
price_col = find_column(df_abonements.columns, ['price', 'цен', 'стоим'])
desc_col = find_column(df_abonements.columns, ['desc', 'опис', 'описание'])
print(f"Используем колонки: name='{name_col}', price='{price_col}', description='{desc_col}'")
for _, row in df_abonements.iterrows():
price = extract_price(row[price_col])
cursor.execute('''
INSERT INTO abonements (name, price, description)
VALUES (?, ?, ?)
''', (
str(row[name_col]),
price,
str(row[desc_col]) if pd.notna(row[desc_col]) else ''
))
print(f"Загружено {len(df_abonements)} абонементов")
else:
print(f"Файл {abonements_file} не найден")
# Загрузка сертификатов из Excel
certificates_file = 'resource/certificates.xlsx'
if os.path.exists(certificates_file):
df_certificates = pd.read_excel(certificates_file)
print(f"\nЗагружаем сертификаты из {certificates_file}")
print(f"Колонки: {list(df_certificates.columns)}")
# Определяем колонки
name_col = find_column(df_certificates.columns, ['name', 'название', 'имя'])
price_col = find_column(df_certificates.columns, ['price', 'цен', 'стоим'])
desc_col = find_column(df_certificates.columns, ['desc', 'опис', 'описание'])
print(f"Используем колонки: name='{name_col}', price='{price_col}', description='{desc_col}'")
for _, row in df_certificates.iterrows():
price = extract_price(row[price_col])
cursor.execute('''
INSERT INTO certificates (name, price, description)
VALUES (?, ?, ?)
''', (
str(row[name_col]),
price,
str(row[desc_col]) if pd.notna(row[desc_col]) else ''
))
print(f"Загружено {len(df_certificates)} сертификатов")
else:
print(f"Файл {certificates_file} не найден")
conn.commit()
conn.close()
print("\nЗагрузка данных из Excel завершена!")
if __name__ == '__main__':
load_from_excel_to_db()

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Flask==2.3.3
pandas==2.0.3
openpyxl==3.1.2
werkzeug==3.1.6
flask-login==0.6.3

BIN
resource/abonements.xlsx Normal file

Binary file not shown.

BIN
resource/certificates.xlsx Normal file

Binary file not shown.

BIN
resource/services.xlsx Normal file

Binary file not shown.

1151
static/css/style.css Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

14
templates/abonements.html Normal file
View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<h2>Абонементы</h2>
<div class="abonements-list">
{% for abonement in abonements %}
<div class="abonement-item">
<h3>{{ abonement.name }}</h3>
<p>{{ abonement.description }}</p>
<p class="price">{{ abonement.price }} рублей</p>
</div>
{% endfor %}
</div>
{% endblock %}

178
templates/about.html Normal file
View file

@ -0,0 +1,178 @@
{% extends "base.html" %}
{% block content %}
<style>
.about-hero {
text-align: center;
margin-bottom: 2rem;
padding: 2rem;
background: linear-gradient(135deg, #fef9f0 0%, #fff4e6 100%);
border-radius: 20px;
position: relative;
overflow: hidden;
}
.about-hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
position: relative;
display: inline-block;
}
.about-hero p {
font-size: 1.2rem;
color: #666;
max-width: 800px;
margin: 0 auto;
}
.about-content {
background: white;
border-radius: 20px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.about-content h3 {
color: #8B5A2B;
margin-top: 1.5rem;
margin-bottom: 1rem;
font-size: 1.4rem;
position: relative;
padding-left: 1rem;
border-left: 4px solid #d4a373;
}
.about-content h3:first-of-type {
margin-top: 0;
}
.about-content p {
line-height: 1.8;
color: #444;
margin-bottom: 1rem;
text-align: justify;
}
.about-content ul {
list-style: none;
padding-left: 0;
margin: 1rem 0;
}
.about-content li {
padding: 0.5rem 0;
padding-left: 1.8rem;
position: relative;
line-height: 1.6;
color: #555;
}
.about-content li::before {
content: "🐴";
position: absolute;
left: 0;
color: #d4a373;
}
.about-quote {
background: linear-gradient(135deg, #2c1810 0%, #4a2c2c 100%);
color: white;
padding: 2rem;
border-radius: 20px;
margin: 2rem 0;
text-align: center;
font-style: italic;
position: relative;
overflow: hidden;
}
.about-quote::before {
content: '"';
position: absolute;
left: 20px;
top: 10px;
font-size: 80px;
opacity: 0.1;
font-family: serif;
}
.about-quote::after {
content: '"';
position: absolute;
right: 20px;
bottom: 10px;
font-size: 80px;
opacity: 0.1;
font-family: serif;
}
.about-quote p {
font-size: 1.2rem;
line-height: 1.6;
position: relative;
z-index: 1;
}
.about-quote .author {
margin-top: 1rem;
font-size: 0.9rem;
opacity: 0.8;
}
@media (max-width: 768px) {
.about-hero h2 {
font-size: 1.8rem;
}
.about-content {
padding: 1.5rem;
}
.about-content h3 {
font-size: 1.2rem;
}
.about-quote p {
font-size: 1rem;
}
}
</style>
<div class="about-hero">
<h2>🐴 О нашей базе "Конный двор"</h2>
<p>Место, где лошади и люди находят друг друга</p>
</div>
<div class="about-content">
<h3>Кто мы такие?</h3>
<p>"Конный двор" — это уютная конно-спортивная база, расположенная в живописном месте Республики Башкортостан. Мы создали это место для тех, кто хочет прикоснуться к миру лошадей, научиться верховой езде или просто отдохнуть на природе в компании этих удивительных животных.</p>
<h3>Наша миссия</h3>
<p>Мы стремимся подарить каждому посетителю радость общения с лошадьми, помочь раскрыть красоту и пользу верховой езды, создать комфортные условия для занятий и прогулок. Наши инструкторы — профессионалы с многолетним опытом, которые найдут подход как к взрослым, так и к детям.</p>
<h3>Что мы предлагаем?</h3>
<ul>
<li>Обучение верховой езде для начинающих и опытных всадников</li>
<li>Конные прогулки по живописному лесу</li>
<li>Зимние прогулки в санях</li>
<li>Экскурсии с рассказом о лошадях и уходе за ними</li>
<li>Подарочные сертификаты и абонементы</li>
</ul>
<h3>Почему выбирают нас?</h3>
<ul>
<li>Опытные инструкторы с педагогическим образованием</li>
<li>Добрые, ухоженные и обученные лошади</li>
<li>Живописные маршруты для прогулок</li>
<li>Индивидуальный подход к каждому ученику</li>
<li>Доступные цены и гибкая система скидок</li>
</ul>
</div>
<div class="about-quote">
<p>Лошадь — это зеркало души человека. Приходя к нам, вы не просто учитесь ездить верхом, вы учитесь понимать, чувствовать и доверять. И это меняет жизнь.</p>
<div class="author">— Основатель базы "Конный двор"</div>
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<h2>Админ панель</h2>
<div class="admin-dashboard">
<div class="admin-menu">
<div class="admin-card">
<h3>📦 Заказы</h3>
<p>Управление заказами пользователей</p>
<a href="{{ url_for('admin_orders') }}" class="btn btn-primary">Перейти</a>
</div>
<div class="admin-card">
<h3>💬 Отзывы</h3>
<p>Модерация отзывов</p>
<a href="{{ url_for('admin_reviews') }}" class="btn btn-primary">Перейти</a>
</div>
<div class="admin-card">
<h3>👥 Пользователи</h3>
<p>Управление пользователями</p>
<a href="{{ url_for('admin_users') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<h2>Новые сообщения от пользователей</h2>
{% if messages %}
<div class="admin-table">
{% for msg in messages %}
<div class="review-card" style="margin-bottom: 1rem;">
<div class="review-header">
<strong style="color: white;">Заказ #{{ msg.order_id }}</strong>
<span style="color: white;">От: {{ msg.user_name }}</span>
<span style="color: white;">{{ msg.created_at }}</span>
</div>
<div class="review-comment" style="margin: 0.5rem 0; color: white;">{{ msg.message }}</div>
<div class="order-actions" style="margin-top: 0.5rem;">
<a href="{{ url_for('chat', order_id=msg.order_id) }}" class="btn-primary" style="border-radius: 50px; text-decoration: none;">Ответить</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>Новых сообщений нет.</p>
{% endif %}
{% endblock %}

170
templates/admin/orders.html Normal file
View file

@ -0,0 +1,170 @@
{% extends "base.html" %}
{% block content %}
<h2>Управление заказами</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="filters">
<h3>Фильтры и сортировка</h3>
<div class="filter-bar">
<label>Сортировка:</label>
<select onchange="window.location.href='?sort='+this.value+'&status={{ status }}'">
<option value="newest" {% if sort == 'newest' %}selected{% endif %}>Новые сначала</option>
<option value="oldest" {% if sort == 'oldest' %}selected{% endif %}>Старые сначала</option>
<option value="price_high" {% if sort == 'price_high' %}selected{% endif %}>Дорогие сначала</option>
<option value="price_low" {% if sort == 'price_low' %}selected{% endif %}>Дешевые сначала</option>
</select>
<label>Статус:</label>
<select onchange="window.location.href='?sort={{ sort }}&status='+this.value">
<option value="all" {% if status == 'all' %}selected{% endif %}>Все</option>
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Ожидает</option>
<option value="confirmed" {% if status == 'confirmed' %}selected{% endif %}>Подтвержден</option>
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Выполнен</option>
<option value="cancelled" {% if status == 'cancelled' %}selected{% endif %}>Отменен</option>
</select>
</div>
</div>
<div class="admin-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Пользователь</th>
<th>Услуга</th>
<th>Дата</th>
<th>Время</th>
<th>Цена</th>
<th>Скидка</th>
<th>Оплата</th>
<th>Статус</th>
<th>Сообщения</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="orders-table-body">
{% for order in orders %}
<tr id="order-row-{{ order.id }}">
<td>{{ order.id }}</td>
<td>{{ order.username or 'Гость' }}</td>
<td>{{ order.service_name }}</td>
<td>{{ order.date or '-' }}</td>
<td>{{ order.time or '-' }}</td>
<td>{{ order.price }} руб.</td>
<td>
{% if order.discount_applied %}
<span style="color: #28a745;">{{ order.discount_amount }} руб.</span>
{% else %}
—
{% endif %}
</td>
<td class="payment-status-cell" data-order-id="{{ order.id }}">
{% if order.payment_status == 'paid' %}
<span style="color: #28a745;">✅ Оплачено</span>
{% else %}
<span style="color: #ffc107;">⚠️ Не оплачено</span>
{% endif %}
<button class="toggle-payment-btn" data-order-id="{{ order.id }}" style="background: none; border: none; cursor: pointer; margin-left: 5px;">🔄</button>
</td>
<td>
<span class="status status-{{ order.status }}">
{% if order.status == 'pending' %}Ожидает
{% elif order.status == 'confirmed' %}Подтвержден
{% elif order.status == 'completed' %}Выполнен
{% elif order.status == 'cancelled' %}Отменен
{% endif %}
</span>
</td>
<td class="unread-cell" data-order-id="{{ order.id }}">
{% if order.unread_count > 0 %}
<span class="unread-badge" style="background: #dc3545; color: white; border-radius: 50%; padding: 2px 8px; font-size: 12px;">{{ order.unread_count }}</span>
{% else %}
—
{% endif %}
</td>
<td>
<a href="{{ url_for('chat', order_id=order.id) }}" class="chat-link btn-primary btn-sm" data-order-id="{{ order.id }}" style="border-radius: 50px; padding: 0.3rem 0.8rem; text-decoration: none;">Чат</a>
<form method="POST" action="{{ url_for('update_order', order_id=order.id) }}?sort={{ sort }}&status={{ status }}" style="display: inline;">
<select name="status" onchange="this.form.submit()" style="border-radius: 50px; padding: 0.3rem;">
<option value="pending" {% if order.status == 'pending' %}selected{% endif %}>Ожидает</option>
<option value="confirmed" {% if order.status == 'confirmed' %}selected{% endif %}>Подтвержден</option>
<option value="completed" {% if order.status == 'completed' %}selected{% endif %}>Выполнен</option>
<option value="cancelled" {% if order.status == 'cancelled' %}selected{% endif %}>Отменен</option>
</select>
</form>
<form method="POST" action="{{ url_for('delete_order', order_id=order.id) }}?sort={{ sort }}&status={{ status }}" style="display: inline;" onsubmit="return confirm('Удалить заказ?')">
<button type="submit" class="btn-danger btn-sm" style="border-radius: 50px; padding: 0.3rem 0.8rem;">Удалить</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
// Переключение статуса оплаты
document.querySelectorAll('.toggle-payment-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const orderId = this.dataset.orderId;
const response = await fetch('/api/toggle_payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId })
});
const data = await response.json();
const cell = document.querySelector(`.payment-status-cell[data-order-id="${orderId}"]`);
if (data.status === 'paid') {
cell.innerHTML = '<span style="color: #28a745;">✅ Оплачено</span> <button class="toggle-payment-btn" data-order-id="' + orderId + '" style="background: none; border: none; cursor: pointer; margin-left: 5px;">🔄</button>';
} else {
cell.innerHTML = '<span style="color: #ffc107;">⚠️ Не оплачено</span> <button class="toggle-payment-btn" data-order-id="' + orderId + '" style="background: none; border: none; cursor: pointer; margin-left: 5px;">🔄</button>';
}
document.querySelectorAll('.toggle-payment-btn').forEach(btn => {
btn.removeEventListener('click', this);
btn.addEventListener('click', arguments.callee);
});
});
});
async function updateUnreadCounts() {
try {
const response = await fetch('/api/admin_unread_counts');
const data = await response.json();
for (const [orderId, count] of Object.entries(data)) {
const cell = document.querySelector(`.unread-cell[data-order-id="${orderId}"]`);
if (cell) {
if (count > 0) {
cell.innerHTML = `<span class="unread-badge" style="background: #dc3545; color: white; border-radius: 50%; padding: 2px 8px; font-size: 12px;">${count}</span>`;
} else {
cell.innerHTML = '—';
}
}
}
} catch (error) {
console.error('Ошибка обновления:', error);
}
}
document.querySelectorAll('.chat-link').forEach(link => {
link.addEventListener('click', async function(e) {
const orderId = this.dataset.orderId;
await fetch('/api/mark_order_read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId })
});
const cell = document.querySelector(`.unread-cell[data-order-id="${orderId}"]`);
if (cell) cell.innerHTML = '—';
});
});
setInterval(updateUnreadCounts, 10000);
</script>
{% endblock %}

View file

@ -0,0 +1,314 @@
{% extends "base.html" %}
{% block content %}
<h2>Модерация отзывов</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="filters">
<h3>Фильтры и сортировка</h3>
<div class="filter-bar">
<label>Сортировка:</label>
<select onchange="window.location.href='?sort='+this.value+'&status={{ status }}&rating={{ rating }}'">
<option value="newest" {% if sort == 'newest' %}selected{% endif %}>Новые сначала</option>
<option value="oldest" {% if sort == 'oldest' %}selected{% endif %}>Старые сначала</option>
<option value="rating_high" {% if sort == 'rating_high' %}selected{% endif %}>Высокий рейтинг</option>
<option value="rating_low" {% if sort == 'rating_low' %}selected{% endif %}>Низкий рейтинг</option>
</select>
<label>Статус:</label>
<select onchange="window.location.href='?sort={{ sort }}&status='+this.value+'&rating={{ rating }}'">
<option value="all" {% if status == 'all' %}selected{% endif %}>Все</option>
<option value="pending" {% if status == 'pending' %}selected{% endif %}>На модерации</option>
<option value="approved" {% if status == 'approved' %}selected{% endif %}>Одобрены</option>
<option value="rejected" {% if status == 'rejected' %}selected{% endif %}>Отклонены</option>
</select>
<label>Оценка:</label>
<select onchange="window.location.href='?sort={{ sort }}&status={{ status }}&rating='+this.value">
<option value="all" {% if rating == 'all' %}selected{% endif %}>Все оценки</option>
<option value="5" {% if rating == '5' %}selected{% endif %}>⭐⭐⭐⭐⭐</option>
<option value="4" {% if rating == '4' %}selected{% endif %}>⭐⭐⭐⭐</option>
<option value="3" {% if rating == '3' %}selected{% endif %}>⭐⭐⭐</option>
<option value="2" {% if rating == '2' %}selected{% endif %}>⭐⭐</option>
<option value="1" {% if rating == '1' %}selected{% endif %}>⭐</option>
</select>
</div>
</div>
<div class="admin-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Пользователь</th>
<th>Заказ</th>
<th>Оценка</th>
<th>Отзыв</th>
<th>Статус</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for review in reviews %}
<tr class="review-row {% if review.status == 'pending' %}row-pending{% endif %}">
<td>{{ review.id }}</td>
<td><strong>{{ review.username }}</strong></td>
<td>Заказ #{{ review.order_id }}</td>
<td>
<div class="rating">
{% for i in range(5) %}
{% if i < review.rating %}
★
{% else %}
☆
{% endif %}
{% endfor %}
</div>
</td>
<td class="review-comment">
<div class="comment-text">{{ review.comment }}</div>
</td>
<td>
<span class="status-badge status-{{ review.status }}">
{% if review.status == 'pending' %}На модерации
{% elif review.status == 'approved' %}Одобрен
{% elif review.status == 'rejected' %}Отклонен
{% endif %}
</span>
</td>
<td>{{ review.created_at[:16] }}</td>
<td>
<form method="POST" action="{{ url_for('moderate_review', review_id=review.id) }}?sort={{ sort }}&status={{ status }}&rating={{ rating }}" style="display: inline;">
<select name="status" onchange="this.form.submit()" class="status-select">
<option value="pending" {% if review.status == 'pending' %}selected{% endif %}>На модерации</option>
<option value="approved" {% if review.status == 'approved' %}selected{% endif %}>Одобрить</option>
<option value="rejected" {% if review.status == 'rejected' %}selected{% endif %}>Отклонить</option>
</select>
</form>
<form method="POST" action="{{ url_for('delete_review', review_id=review.id) }}?sort={{ sort }}&status={{ status }}&rating={{ rating }}" style="display: inline;" onsubmit="return confirm('Удалить отзыв?')">
<button type="submit" class="btn btn-danger btn-sm">Удалить</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not reviews %}
<div class="empty-state">
<p>Нет отзывов для отображения</p>
</div>
{% endif %}
</div>
<style>
.filters {
background: white;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.filters h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #4a2c2c;
font-size: 1.2rem;
}
.filter-bar {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
align-items: center;
}
.filter-bar label {
font-weight: bold;
color: #4a2c2c;
}
.filter-bar select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
}
.admin-table {
overflow-x: auto;
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.admin-table table {
width: 100%;
border-collapse: collapse;
}
.admin-table th,
.admin-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
vertical-align: top;
}
.admin-table th {
background-color: #4a2c2c;
color: white;
font-weight: bold;
}
.admin-table tr:hover {
background-color: #f5f5f5;
}
.row-pending {
background-color: #fff3e0;
}
.rating {
color: #ffc107;
font-size: 1.1rem;
letter-spacing: 2px;
}
.review-comment {
max-width: 300px;
}
.comment-text {
word-wrap: break-word;
line-height: 1.4;
margin-bottom: 0.25rem;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
text-align: center;
}
.status-pending {
background-color: #ffc107;
color: #856404;
}
.status-approved {
background-color: #28a745;
color: white;
}
.status-rejected {
background-color: #dc3545;
color: white;
}
.status-select {
padding: 0.3rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.85rem;
margin-bottom: 0.25rem;
cursor: pointer;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
display: inline-block;
margin: 0.25rem 0;
text-decoration: none;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #666;
}
.alert {
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
@media (max-width: 768px) {
.filter-bar {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.filter-bar select {
width: 100%;
}
.admin-table th,
.admin-table td {
padding: 0.5rem;
font-size: 0.85rem;
}
.review-comment {
max-width: 200px;
}
.status-select {
width: 100%;
margin-bottom: 0.5rem;
}
.btn-sm {
width: 100%;
text-align: center;
}
}
</style>
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block content %}
<h2>Управление пользователями</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="admin-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Имя пользователя</th>
<th>Email</th>
<th>Роль</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email or '-' }}</td>
<td>
{% if user.role == 'admin' %}
<span class="badge badge-admin">Админ</span>
{% else %}
<span class="badge badge-user">Пользователь</span>
{% endif %}
</td>
<td>{{ user.created_at[:10] }}</td>
<td>
{% if user.role != 'admin' %}
<form method="POST" action="{{ url_for('make_admin', user_id=user.id) }}" style="display: inline;">
<button type="submit" class="btn btn-warning btn-sm">Сделать админом</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_user', user_id=user.id) }}" style="display: inline;" onsubmit="return confirm('Удалить пользователя?')">
<button type="submit" class="btn btn-danger btn-sm">Удалить</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

137
templates/base.html Normal file
View file

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Конный двор - Конно-спортивная база</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script>
function setUserTimezone() {
const timezoneOffset = new Date().getTimezoneOffset();
fetch('/api/set_timezone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timezone_offset: -timezoneOffset })
}).catch(() => {});
}
document.addEventListener('DOMContentLoaded', setUserTimezone);
</script>
</head>
<body>
<div class="wrapper">
<header>
<h1>🐎 Конно-спортивная база "Конный двор" 🐎</h1>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Главная</a></li>
<li><a href="{{ url_for('about') }}">О себе</a></li>
<li><a href="{{ url_for('services') }}">Услуги</a></li>
<li><a href="{{ url_for('abonements') }}">Абонементы</a></li>
<li><a href="{{ url_for('certificates') }}">Сертификаты</a></li>
<li><a href="{{ url_for('gallery') }}">Фотогалерея</a></li>
<li><a href="{{ url_for('discounts') }}">Скидки</a></li>
<li><a href="{{ url_for('reviews') }}">Отзывы</a></li>
{% if session.user_id %}
<li><a href="{{ url_for('order') }}">Заказать</a></li>
<li><a href="{{ url_for('my_orders') }}" id="messages-link">
Мои заказы
<span id="unread-badge" style="background: #dc3545; color: white; border-radius: 50%; padding: 2px 6px; font-size: 10px; display: none;">0</span>
</a></li>
{% if session.role == 'admin' %}
<li><a href="{{ url_for('admin_panel') }}">Админ панель</a></li>
<li><a href="{{ url_for('admin_messages') }}"> Сообщения</a></li>
{% endif %}
<li><a href="{{ url_for('logout') }}">Выйти ({{ session.username }})</a></li>
{% else %}
<li><a href="{{ url_for('login') }}">Вход</a></li>
<li><a href="{{ url_for('register') }}">Регистрация</a></li>
{% endif %}
</ul>
</nav>
</header>
<main class="content">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>
<div class="footer-content">
<div class="footer-section">
<h4>Контакты</h4>
<p>📞 +7 962 545 066</p>
<p>✉️ info@koniydvor.ru</p>
</div>
<div class="footer-section">
<h4>Адрес</h4>
<p>Республика Башкортостан, Белорецкий район,<br>ул. Благой ключ 101</p>
</div>
<div class="footer-section">
<h4>Часы работы</h4>
<p>Ежедневно: 9:00 – 20:00</p>
<p>Без выходных</p>
</div>
</div>
<div class="footer-bottom">
<p>© 2026 Конно-спортивная база "Конный двор"</p>
<p class="team-credit">🐎 Разработано командой Ганвестzxc 🐎</p>
<p class="footer-joke" style="font-size: 0.6rem; opacity: 0.5; margin-top: 0.5rem;">
</p>
</div>
</footer>
</div>
<style>
/* базовые стили — уже есть в style.css, здесь только самые основные на всякий случай */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Montserrat', sans-serif; background: url('/static/images/background.jpg') center/cover fixed; }
body::before { content: ''; position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: -1; }
.wrapper { min-height: 100vh; display: flex; flex-direction: column; }
header { background: rgba(44,24,16,0.92); border-bottom: 2px solid #d4a373; padding: 1rem 0; position: sticky; top: 0; z-index: 1000; }
header h1 { font-family: 'Playfair Display', serif; font-size: 2rem; text-align: center; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
nav ul { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.5rem; list-style: none; margin-top: 1rem; }
nav a { color: white; text-decoration: none; padding: 0.6rem 1.2rem; border-radius: 50px; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); transition: 0.2s; }
nav a:hover { background: #d4a373; color: #2c1810; transform: translateY(-2px); }
.content { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; width: 100%; flex: 1; }
h2 { font-family: 'Playfair Display', serif; color: #f5cba0; margin-bottom: 1.5rem; font-size: 2rem; border-left: 4px solid #f5cba0; padding-left: 1rem; }
footer { background: rgba(30,16,10,0.98); border-top: 3px solid #d4a373; padding: 2rem 0 1rem; margin-top: auto; }
.footer-content { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px,1fr)); gap: 1.5rem; padding: 0 1rem; }
.footer-section h4 { color: #ffdd99; font-size: 1.25rem; margin-bottom: 1rem; }
.footer-section p { color: #f0e2d0; margin: 0.5rem 0; }
.footer-bottom { text-align: center; padding: 1.5rem 1rem 0.5rem; border-top: 1px solid rgba(212,163,115,0.4); margin-top: 1.5rem; color: #ddc8aa; }
.team-credit { font-size: 0.75rem; margin-top: 0.5rem; color: #c9a87c; }
@media (max-width: 768px) {
header h1 { font-size: 1.2rem; }
nav a { padding: 0.3rem 0.6rem; font-size: 0.85rem; }
h2 { font-size: 1.5rem; }
.footer-content { grid-template-columns: 1fr; text-align: center; }
}
</style>
<script>
function checkUnreadMessages() {
fetch('/api/unread_count')
.then(res => res.json())
.then(data => {
const badge = document.getElementById('unread-badge');
if (badge && data.count > 0) {
badge.textContent = data.count;
badge.style.display = 'inline-block';
} else if (badge) badge.style.display = 'none';
})
.catch(e => console.error(e));
}
{% if session.user_id %}
setInterval(checkUnreadMessages, 10000);
checkUnreadMessages();
{% endif %}
</script>
</body>
</html>

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<h2>Подарочные сертификаты</h2>
<div class="certificates-list">
{% for certificate in certificates %}
<div class="certificate-item">
<h3>{{ certificate.name }}</h3>
<p>{{ certificate.description }}</p>
<p class="price">{{ certificate.price }} рублей</p>
</div>
{% endfor %}
</div>
{% endblock %}

237
templates/chat.html Normal file
View file

@ -0,0 +1,237 @@
{% extends "base.html" %}
{% block content %}
<style>
.chat-container {
max-width: 800px;
margin: 0 auto;
background: rgba(0, 0, 0, 0.55);
border-radius: 20px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.order-info {
background: rgba(0, 0, 0, 0.3);
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.order-info h3 {
color: #f5cba0;
margin-bottom: 0.5rem;
}
.order-info p {
color: white;
margin: 0.3rem 0;
}
.order-info strong {
color: #f5cba0;
}
.chat-messages {
height: 400px;
overflow-y: auto;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
}
.message {
margin-bottom: 0.8rem;
padding: 0.6rem 1rem;
border-radius: 12px;
max-width: 80%;
}
.message-user {
background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%);
color: #2c1810;
margin-left: auto;
text-align: right;
}
.message-admin {
background: rgba(44, 24, 16, 0.85);
color: white;
}
.message-system {
background: rgba(255, 255, 255, 0.2);
color: #f5cba0;
text-align: center;
max-width: 100%;
font-style: italic;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.3rem;
font-size: 0.75rem;
}
.message-time {
opacity: 0.7;
}
.chat-input {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.3);
}
.chat-input textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
resize: vertical;
font-family: inherit;
}
.chat-input button {
margin-top: 0.5rem;
width: 100%;
}
.status {
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
}
.status-pending { background: #ffc107; color: #856404; }
.status-confirmed { background: #17a2b8; color: white; }
.status-completed { background: #28a745; color: white; }
.status-cancelled { background: #dc3545; color: white; }
.btn-primary {
background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%);
color: #2c1810;
padding: 0.5rem 1rem;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, #ffdd99 0%, #f5cba0 100%);
transform: translateY(-2px);
}
@media (max-width: 768px) {
.message {
max-width: 95%;
}
}
</style>
<h2>Чат по заказу #{{ order.id }}</h2>
<div class="chat-container">
<div class="order-info">
<h3>Информация о заказе</h3>
<p><strong>Услуга:</strong> {{ order.service_name }}</p>
<p><strong>Дата:</strong> {{ order.date }} в {{ order.time }}</p>
<p><strong>Количество человек:</strong> {{ order.persons }}</p>
<p><strong>Цена:</strong>
{% if order.discount_applied %}
<span style="text-decoration: line-through; color: #aaa;">{{ order.original_price }} руб.</span>
<span style="color: #28a745;">{{ order.price }} руб.</span>
<span style="background: #28a745; color: white; padding: 0.2rem 0.5rem; border-radius: 20px; font-size: 0.7rem; margin-left: 0.5rem;">Скидка 15%</span>
{% else %}
{{ order.price }} руб.
{% endif %}
</p>
<p><strong>Статус:</strong> <span class="status status-{{ order.status }}">{{ order.status }}</span></p>
{% if order.comment %}
<p><strong>Ваш комментарий:</strong> {{ order.comment }}</p>
{% endif %}
</div>
<div class="chat-messages" id="chatMessages">
{% if messages %}
{% for message in messages %}
<div class="message {% if message.sender_type == 'system' %}message-system{% elif message.sender_type == 'admin' %}message-admin{% else %}message-user{% endif %}">
<div class="message-header">
<strong>
{% if message.sender_type == 'system' %}
📢 Система
{% elif message.sender_type == 'admin' %}
👑 Администратор
{% else %}
👤 {{ message.sender_name }}
{% endif %}
</strong>
<span class="message-time">{{ message.created_at }}</span>
</div>
<div class="message-text">{{ message.message }}</div>
</div>
{% endfor %}
{% else %}
<div class="message message-system">
<div class="message-text">Нет сообщений. Напишите что-нибудь...</div>
</div>
{% endif %}
</div>
<div class="chat-input">
<form id="messageForm">
<input type="hidden" id="order_id" value="{{ order.id }}">
<textarea id="message" placeholder="Введите сообщение..." rows="3" required></textarea>
<button type="submit" class="btn-primary">Отправить</button>
</form>
</div>
</div>
<script>
// Отмечаем сообщения как прочитанные
fetch('/api/mark_read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: {{ order.id }} })
});
// Отправка сообщения
document.getElementById('messageForm').addEventListener('submit', async function(e) {
e.preventDefault();
const orderId = document.getElementById('order_id').value;
const message = document.getElementById('message').value.trim();
if (!message) return;
const formData = new FormData();
formData.append('order_id', orderId);
formData.append('message', message);
const response = await fetch('/send_message', {
method: 'POST',
body: formData
});
if (response.ok) {
document.getElementById('message').value = '';
location.reload();
} else {
alert('Ошибка при отправке сообщения');
}
});
// Автоскролл вниз
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
// Обновляем чат каждые 5 секунд
setInterval(() => {
location.reload();
}, 100000);
</script>
{% endblock %}

66
templates/discounts.html Normal file
View file

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block content %}
<h2>Скидки и специальные предложения</h2>
<div class="discounts-section">
<div class="discounts-intro">
<p>Мы хотим, чтобы общение с лошадьми было доступным для всех! Поэтому у нас действует гибкая система скидок и специальных предложений.</p>
</div>
<div class="discounts-grid">
<div class="discount-card">
<div class="discount-icon">🎂</div>
<h3>Скидка в честь дня рождения</h3>
<div class="discount-value">10%</div>
<p>При предъявлении документа, подтверждающего день рождения (скидка действует в день рождения и 3 дня после)</p>
</div>
<div class="discount-card">
<div class="discount-icon">👶</div>
<h3>Детям до 5 лет</h3>
<div class="discount-value">Бесплатно</div>
<p>Прогулки в санях для детей до 5 лет - бесплатно в сопровождении родителей</p>
</div>
<div class="discount-card">
<div class="discount-icon">👨‍👩‍👧‍👦</div>
<h3>Семейные прогулки</h3>
<div class="discount-value">15%</div>
<p>При заказе прогулки для семьи из 4-х человек и более</p>
</div>
<div class="discount-card">
<div class="discount-icon">📅</div>
<h3>Абонементы</h3>
<div class="discount-value">Экономия до 20%</div>
<p>При покупке абонемента вы экономите до 20% от стоимости разовых занятий</p>
</div>
<div class="discount-card">
<div class="discount-icon">🎓</div>
<h3>Студентам и школьникам</h3>
<div class="discount-value">10%</div>
<p>При предъявлении студенческого билета или справки из школы</p>
</div>
<div class="discount-card">
<div class="discount-icon">👴</div>
<h3>Пенсионерам</h3>
<div class="discount-value">10%</div>
<p>При предъявлении пенсионного удостоверения</p>
</div>
</div>
<div class="discounts-note">
<h3>Важно!</h3>
<p>Скидки не суммируются. Вы можете выбрать наиболее выгодное для вас предложение.
Для получения скидки необходимо предъявить подтверждающий документ перед началом прогулки или занятия.</p>
</div>
<div class="special-offer">
<h3>Специальное предложение для именинников!</h3>
<p>В свой день рождения вы можете прийти на прогулку с компанией до 3-х человек и получить скидку 10% для всей компании!</p>
</div>
</div>
{% endblock %}

34
templates/gallery.html Normal file
View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block content %}
<h2>Фотогалерея</h2>
<div class="gallery-intro">
<p>Здесь вы можете увидеть фотографии нашей базы, лошадей и счастливых посетителей. Приходите к нам, чтобы создать свои яркие воспоминания!</p>
</div>
<div class="gallery-grid">
{% if images %}
{% for image in images %}
<div class="gallery-item">
<img src="{{ url_for('static', filename='images/gallery/' + image) }}" alt="Фото с базы Конный двор">
<p class="gallery-caption">{{ image.replace('_', ' ').replace('-', ' ').split('.')[0] }}</p>
</div>
{% endfor %}
{% else %}
<div class="gallery-item placeholder">
<div class="gallery-placeholder">
<span class="placeholder-icon">📷</span>
<p>Фото скоро появятся</p>
</div>
<p class="gallery-caption">Добавьте фотографии в папку static/images/gallery/</p>
</div>
{% endif %}
</div>
{% if images %}
<div class="gallery-note">
<p>Всего фотографий: {{ images|length }}. Хотите поделиться своими фотографиями? Отмечайте нас в социальных сетях с хештегом #КонныйДворБелорецк</p>
</div>
{% endif %}
{% endblock %}

58
templates/index.html Normal file
View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block content %}
<style>
.hero {
text-align: center;
padding: 4rem 2rem;
background: rgba(0, 0, 0, 0.5) !important;
backdrop-filter: blur(8px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.hero h2 {
color: white !important;
font-size: 2.8rem;
margin-bottom: 1.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
border-left: none;
}
.about-text {
max-width: 800px;
margin: 1.5rem auto;
font-size: 1.2rem;
line-height: 1.8;
color: white !important;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
.about-text p {
color: white !important;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.hero {
padding: 2rem 1rem;
}
.hero h2 {
font-size: 1.8rem;
}
.about-text {
font-size: 1rem;
}
}
</style>
<section class="hero">
<h2>Добро пожаловать в "Конный двор"!</h2>
<div class="about-text">
<p>Это база где вы сможете поближе познакомиться с лошадьми, обучиться навыкам верховной езды, подышать свежим воздухом.</p>
<p>Наша база занимается проведением прогулок верхом и в санях, а также обучением навыкам верховной езды. Приходите к нам и вы откроете для себя удивительный мир лошадей и отдыха!</p>
</div>
</section>
{% endblock %}

31
templates/login.html Normal file
View file

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block content %}
<div class="auth-container">
<h2>Вход в систему</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" class="auth-form">
<div class="form-group">
<label for="username">Имя пользователя:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Войти</button>
</form>
<p class="auth-link">Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрироваться</a></p>
</div>
{% endblock %}

281
templates/my_orders.html Normal file
View file

@ -0,0 +1,281 @@
{% extends "base.html" %}
{% block content %}
<style>
.orders-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
margin-bottom: 2rem;
gap: 1rem;
}
.orders-header h2 { margin-bottom: 0; }
.orders-stats {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.orders-stats .stat-badge {
background: rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 0.3rem 0.8rem;
font-size: 0.8rem;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.orders-filters {
background: rgba(0, 0, 0, 0.55);
padding: 1rem;
border-radius: 20px;
margin-bottom: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.filter-bar {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.filter-bar label {
font-size: 0.85rem;
font-weight: 500;
color: #f5cba0;
}
.filter-bar select {
padding: 0.4rem 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
cursor: pointer;
color: #2c2c2c;
}
.orders-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.order-card {
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
overflow: hidden;
transition: all 0.2s ease;
}
.order-card:hover {
transform: translateX(5px);
background: rgba(0, 0, 0, 0.6);
}
.order-header {
background: rgba(0, 0, 0, 0.3);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.order-header h3 {
margin: 0;
font-size: 1.1rem;
color: #f5cba0;
}
.order-details {
padding: 1rem;
}
.order-details p {
margin: 0.5rem 0;
color: white;
}
.order-details strong {
color: #f5cba0;
min-width: 120px;
display: inline-block;
}
.discount-badge {
background: #28a745;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 20px;
font-size: 0.7rem;
display: inline-block;
margin-left: 0.5rem;
}
.original-price {
text-decoration: line-through;
color: #aaa;
margin-right: 0.5rem;
}
.payment-info {
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.payment-note {
font-size: 0.7rem;
color: #ccc;
margin-top: 0.3rem;
}
.order-actions {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.order-actions .btn-primary {
background: #f5cba0;
color: #2c1810;
border: none;
border-radius: 50px;
padding: 0.5rem 1rem;
text-decoration: none;
display: inline-block;
font-weight: bold;
transition: 0.2s;
}
.order-actions .btn-primary:hover {
background: #ffdd99;
transform: translateY(-2px);
}
.status {
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.status-pending { background: #ffc107; color: #856404; }
.status-confirmed { background: #17a2b8; color: white; }
.status-completed { background: #28a745; color: white; }
.status-cancelled { background: #dc3545; color: white; }
.unread-badge {
background: #dc3545;
color: white;
border-radius: 50%;
padding: 2px 6px;
font-size: 10px;
margin-left: 5px;
}
.empty-orders {
text-align: center;
padding: 2rem;
background: rgba(0, 0, 0, 0.55);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.empty-orders .icon { font-size: 3rem; margin-bottom: 0.5rem; }
.empty-orders h3 { color: #f5cba0; margin-bottom: 0.5rem; }
.empty-orders p { color: white; }
@media (max-width: 768px) {
.orders-header { flex-direction: column; align-items: flex-start; }
.order-details strong { min-width: auto; display: inline; }
.filter-bar { flex-direction: column; align-items: flex-start; }
.filter-bar select { width: 100%; }
}
</style>
<div class="orders-header">
<h2>Мои заказы</h2>
<div class="orders-stats">
{% set total = orders|length %}
{% set pending = orders|selectattr('status', 'equalto', 'pending')|list|length %}
{% set confirmed = orders|selectattr('status', 'equalto', 'confirmed')|list|length %}
{% set completed = orders|selectattr('status', 'equalto', 'completed')|list|length %}
<span class="stat-badge">Всего: {{ total }}</span>
<span class="stat-badge">Ожидают: {{ pending }}</span>
<span class="stat-badge">Подтверждены: {{ confirmed }}</span>
<span class="stat-badge">Выполнены: {{ completed }}</span>
</div>
</div>
<div class="orders-filters">
<div class="filter-bar">
<label>Сортировка:</label>
<select onchange="window.location.href='?sort='+this.value+'&status={{ status }}'">
<option value="newest" {% if sort == 'newest' %}selected{% endif %}>Новые сначала</option>
<option value="oldest" {% if sort == 'oldest' %}selected{% endif %}>Старые сначала</option>
<option value="price_high" {% if sort == 'price_high' %}selected{% endif %}>Дорогие сначала</option>
<option value="price_low" {% if sort == 'price_low' %}selected{% endif %}>Дешевые сначала</option>
</select>
<label>Статус:</label>
<select onchange="window.location.href='?sort={{ sort }}&status='+this.value">
<option value="all" {% if status == 'all' %}selected{% endif %}>Все</option>
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Ожидает</option>
<option value="confirmed" {% if status == 'confirmed' %}selected{% endif %}>Подтвержден</option>
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Выполнен</option>
<option value="cancelled" {% if status == 'cancelled' %}selected{% endif %}>Отменен</option>
</select>
</div>
</div>
{% if orders %}
<div class="orders-list">
{% for order in orders %}
<div class="order-card">
<div class="order-header">
<h3>Заказ #{{ order.id }}</h3>
<span class="status status-{{ order.status }}">
{% if order.status == 'pending' %}Ожидает
{% elif order.status == 'confirmed' %}Подтвержден
{% elif order.status == 'completed' %}Выполнен
{% elif order.status == 'cancelled' %}Отменен
{% endif %}
</span>
</div>
<div class="order-details">
<p><strong>Услуга:</strong> {{ order.service_name }}</p>
{% if order.date and order.time %}
<p><strong>Дата:</strong> {{ order.date }} в {{ order.time }}</p>
{% endif %}
<p><strong>Количество человек:</strong> {{ order.persons }}</p>
<p><strong>Цена:</strong>
{% if order.discount_applied %}
<span class="original-price">{{ order.original_price }} руб.</span>
<span style="color: #28a745;">{{ order.price }} руб.</span>
<span class="discount-badge">Скидка {{ order.discount_amount }} руб. (15%)</span>
{% else %}
{{ order.price }} руб.
{% endif %}
</p>
<div class="payment-info">
<p><strong>💳 Оплата:</strong>
{% if order.payment_status == 'paid' %}
<span style="color: #28a745;">✅ Оплачено</span>
{% else %}
<span style="color: #ffc107;">⚠️ Не оплачено</span>
{% endif %}
</p>
<p><strong>📞 Реквизиты для оплаты:</strong> +7 962 545 066 (СБП, перевод по номеру телефона)</p>
<p><strong>💵 Наличные:</strong> на месте встречи</p>
<p class="payment-note">Оплата производится при получении услуги или заранее по номеру телефона. После оплаты администратор подтвердит статус.</p>
</div>
{% if order.comment %}
<p><strong>Комментарий:</strong> {{ order.comment }}</p>
{% endif %}
<p><strong>Дата заказа:</strong> {{ order.created_at }}</p>
<div class="order-actions">
<a href="{{ url_for('chat', order_id=order.id) }}" class="btn-primary">
Чат с поддержкой
{% if order.unread_count > 0 %}
<span class="unread-badge">{{ order.unread_count }}</span>
{% endif %}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-orders">
<div class="icon">🐴</div>
<h3>У вас пока нет заказов</h3>
<p>Хотите прокатиться верхом или обучиться верховой езде?</p>
<a href="{{ url_for('order') }}" class="btn-primary" style="display: inline-block; margin-top: 1rem; text-decoration: none;">Сделать первый заказ</a>
</div>
{% endif %}
{% endblock %}

182
templates/order_form.html Normal file
View file

@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block content %}
<h2>Оформление заказа</h2>
<style>
.order-form {
max-width: 600px;
margin: 0 auto;
background: rgba(0, 0, 0, 0.55);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; color: #f5cba0; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 0.5rem;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 8px;
background: rgba(255,255,255,0.9);
}
.form-group textarea { resize: none; height: 100px; }
.price-display {
font-size: 1.3rem; font-weight: bold; color: #f5cba0;
padding: 0.5rem; background: rgba(0,0,0,0.3);
border-radius: 8px; text-align: center;
}
.btn-primary {
background: #f5cba0;
color: #2c1810;
padding: 0.75rem;
border: none;
border-radius: 50px;
cursor: pointer;
width: 100%;
font-weight: bold;
transition: 0.2s;
}
.btn-primary:hover {
background: #ffdd99;
transform: translateY(-2px);
}
small {
display: block;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #ffffff !important;
}
</style>
<form method="POST" class="order-form" id="orderForm">
<div class="form-group">
<label for="service_type">Тип услуги:</label>
<select id="service_type" name="service_type" required>
<option value="">Выберите тип</option>
<option value="service">Услуга</option>
<option value="abonement">Абонемент</option>
<option value="certificate">Подарочный сертификат</option>
</select>
</div>
<div class="form-group">
<label for="service_id">Выберите:</label>
<select id="service_id" name="service_id" required disabled>
<option value="">Сначала выберите тип услуги</option>
</select>
</div>
<div class="form-group">
<label>Цена:</label>
<div id="price_display" class="price-display">0 руб.</div>
<input type="hidden" id="price" name="price" value="0">
<input type="hidden" id="service_name" name="service_name">
</div>
<div id="date-group" class="form-group">
<label for="date">Дата:</label>
<input type="date" id="date" name="date">
<small>Нельзя выбрать прошедшую дату</small>
</div>
<div id="time-group" class="form-group">
<label for="time">Время:</label>
<input type="time" id="time" name="time">
<small id="time_hint">Режим работы: с 9:00 до 20:00</small>
</div>
<div id="persons-group" class="form-group">
<label for="persons">Количество человек:</label>
<input type="number" id="persons" name="persons" min="1" max="20" step="1" value="1">
<small>от 1 до 20. При заказе прогулки для 4+ человек - скидка 15%</small>
</div>
<div class="form-group">
<label for="comment">Комментарий к заказу:</label>
<textarea id="comment" name="comment" rows="3" placeholder="Ваши пожелания..."></textarea>
</div>
<button type="submit" class="btn-primary">Оформить заказ</button>
</form>
<script>
let itemsData = {services: [], abonements: [], certificates: []};
fetch('/api/services')
.then(res => res.json())
.then(data => { itemsData = data; })
.catch(console.error);
const typeSelect = document.getElementById('service_type');
const serviceSelect = document.getElementById('service_id');
const dateGroup = document.getElementById('date-group');
const timeGroup = document.getElementById('time-group');
const personsGroup = document.getElementById('persons-group');
const timeHint = document.getElementById('time_hint');
typeSelect.addEventListener('change', function() {
const type = this.value;
if (type === 'certificate') {
dateGroup.style.display = 'none';
timeGroup.style.display = 'none';
personsGroup.style.display = 'none';
} else {
dateGroup.style.display = 'block';
timeGroup.style.display = 'block';
personsGroup.style.display = 'block';
}
if (!type) {
serviceSelect.disabled = true;
serviceSelect.innerHTML = '<option value="">Сначала выберите тип услуги</option>';
document.getElementById('price_display').textContent = '0 руб.';
document.getElementById('price').value = 0;
return;
}
let items = [];
if (type === 'service') items = itemsData.services;
else if (type === 'abonement') items = itemsData.abonements;
else if (type === 'certificate') items = itemsData.certificates;
serviceSelect.disabled = false;
serviceSelect.innerHTML = '<option value="">Выберите...</option>';
items.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = `${item.name} - ${item.price} руб.`;
option.dataset.price = item.price;
option.dataset.name = item.name;
if (item.duration) option.dataset.duration = item.duration;
serviceSelect.appendChild(option);
});
});
serviceSelect.addEventListener('change', function() {
const opt = this.options[this.selectedIndex];
if (opt.value) {
document.getElementById('price_display').textContent = opt.dataset.price + ' руб.';
document.getElementById('price').value = opt.dataset.price;
document.getElementById('service_name').value = opt.dataset.name;
const duration = opt.dataset.duration;
if (duration && typeSelect.value === 'service') {
let minutes = 0;
if (duration.includes('минут')) minutes = parseInt(duration);
else if (duration.includes('час')) minutes = parseInt(duration) * 60;
if (minutes > 0) {
const maxHour = Math.floor((20*60 - minutes) / 60);
const maxMin = (20*60 - minutes) % 60;
timeHint.innerHTML = `Услуга длится ${duration}. Необходимо закончить до 20:00. Максимальное время начала: ${maxHour}:${maxMin.toString().padStart(2,'0')}`;
} else timeHint.innerHTML = 'Режим работы: с 9:00 до 20:00';
} else timeHint.innerHTML = 'Режим работы: с 9:00 до 20:00';
} else {
document.getElementById('price_display').textContent = '0 руб.';
document.getElementById('price').value = 0;
}
});
document.getElementById('date').min = new Date().toISOString().split('T')[0];
</script>
{% endblock %}

120
templates/register.html Normal file
View file

@ -0,0 +1,120 @@
{% extends "base.html" %}
{% block content %}
<style>
.auth-container {
max-width: 500px;
margin: 2rem auto;
padding: 2rem;
background: rgba(0, 0, 0, 0.55);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.auth-container h2 {
color: #f5cba0;
margin-bottom: 1.5rem;
text-align: center;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.3rem;
font-weight: 600;
color: #f5cba0;
}
.form-group input {
width: 100%;
padding: 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
font-size: 1rem;
background: rgba(255, 255, 255, 0.9);
color: #2c2c2c;
}
.form-group input:focus {
outline: none;
border-color: #f5cba0;
}
.btn-primary {
width: 100%;
background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%);
color: #2c1810;
padding: 0.75rem;
border: none;
border-radius: 50px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, #ffdd99 0%, #f5cba0 100%);
transform: translateY(-2px);
}
.auth-link {
text-align: center;
margin-top: 1rem;
color: white;
}
.auth-link a {
color: #f5cba0;
text-decoration: none;
}
.auth-link a:hover {
text-decoration: underline;
}
small {
display: block;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #f5cba0;
}
</style>
<div class="auth-container">
<h2>Регистрация</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" class="auth-form">
<div class="form-group">
<label for="username">Имя пользователя:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email (необязательно):</label>
<input type="email" id="email" name="email" placeholder="example@mail.ru">
<small>Формат: name@domain.ru</small>
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn-primary">Зарегистрироваться</button>
</form>
<p class="auth-link">Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p>
</div>
{% endblock %}

297
templates/reviews.html Normal file
View file

@ -0,0 +1,297 @@
{% extends "base.html" %}
{% block content %}
<style>
.reviews-section {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
.add-review {
background: rgba(0, 0, 0, 0.55);
padding: 1.5rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
height: fit-content;
}
.add-review h3 {
color: #f5cba0;
margin-bottom: 1rem;
}
.reviews-list {
background: rgba(0, 0, 0, 0.55);
padding: 1.5rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
max-height: 600px;
overflow-y: auto;
}
.reviews-list h3 {
color: #f5cba0;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.3rem;
font-weight: 600;
color: #f5cba0;
}
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
color: #2c2c2c;
}
.form-group textarea {
resize: none;
height: 100px;
}
.review-card {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.2s ease;
}
.review-card:hover {
background: rgba(0, 0, 0, 0.4);
transform: translateX(5px);
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.review-header strong {
color: #f5cba0;
font-weight: 600;
}
.rating {
color: #ffc107;
font-size: 1rem;
letter-spacing: 1px;
}
.review-date {
color: #d4a373;
font-size: 0.7rem;
margin-bottom: 0.5rem;
}
.review-comment {
color: white;
line-height: 1.5;
font-size: 0.95rem;
}
.filters {
background: rgba(0, 0, 0, 0.3);
padding: 1rem;
border-radius: 12px;
margin-bottom: 1.5rem;
}
.filter-bar {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.filter-bar label {
font-size: 0.85rem;
font-weight: 500;
color: #f5cba0;
}
.filter-bar select {
padding: 0.4rem 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
cursor: pointer;
color: #2c2c2c;
}
.empty-reviews {
text-align: center;
padding: 2rem;
margin-top: 1rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
}
.empty-reviews p {
color: white;
font-size: 1rem;
}
.btn-primary {
background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%);
color: #2c1810;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, #ffdd99 0%, #f5cba0 100%);
transform: translateY(-2px);
}
.info {
color: #f5cba0;
text-align: center;
padding: 1rem;
}
@media (max-width: 768px) {
.reviews-section {
grid-template-columns: 1fr;
gap: 1rem;
}
.add-review {
order: 2;
}
.reviews-list {
order: 1;
max-height: none;
}
.filter-bar {
flex-direction: column;
align-items: flex-start;
}
.filter-bar select {
width: 100%;
}
}
</style>
<h2>Отзывы наших посетителей</h2>
<div class="reviews-section">
<!-- Форма добавления отзыва -->
<div class="add-review">
<h3>Оставить отзыв</h3>
{% if session.user_id %}
{% if completed_orders %}
<form method="POST" class="review-form">
<div class="form-group">
<label for="order_id">Выберите заказ:</label>
<select id="order_id" name="order_id" required>
<option value="">Выберите выполненный заказ</option>
{% for order in completed_orders %}
<option value="{{ order.id }}">
Заказ #{{ order.id }} - {{ order.service_name }} ({{ order.date }})
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="rating">Оценка:</label>
<select id="rating" name="rating" required>
<option value="5">5 - Отлично</option>
<option value="4">4 - Хорошо</option>
<option value="3">3 - Удовлетворительно</option>
<option value="2">2 - Плохо</option>
<option value="1">1 - Ужасно</option>
</select>
</div>
<div class="form-group">
<label for="comment">Ваш отзыв:</label>
<textarea id="comment" name="comment" rows="4" placeholder="Расскажите о вашем опыте..." required></textarea>
</div>
<button type="submit" class="btn-primary">Отправить отзыв</button>
</form>
{% else %}
<p class="info">У вас нет выполненных заказов, на которые можно оставить отзыв.</p>
{% endif %}
{% else %}
<p class="info">Чтобы оставить отзыв, пожалуйста, <a href="{{ url_for('login') }}" style="color: #f5cba0;">войдите</a> в систему.</p>
{% endif %}
</div>
<!-- Список отзывов -->
<div class="reviews-list">
<div class="filters">
<h3>Отзывы</h3>
<div class="filter-bar">
<label>Сортировка:</label>
<select onchange="window.location.href='?sort='+this.value+'&rating={{ rating_filter }}'">
<option value="newest" {% if sort == 'newest' %}selected{% endif %}>Новые сначала</option>
<option value="oldest" {% if sort == 'oldest' %}selected{% endif %}>Старые сначала</option>
<option value="rating_high" {% if sort == 'rating_high' %}selected{% endif %}>По рейтингу (высокий)</option>
<option value="rating_low" {% if sort == 'rating_low' %}selected{% endif %}>По рейтингу (низкий)</option>
</select>
<label>Фильтр по оценке:</label>
<select onchange="window.location.href='?sort={{ sort }}&rating='+this.value">
<option value="all" {% if rating_filter == 'all' %}selected{% endif %}>Все</option>
<option value="5" {% if rating_filter == '5' %}selected{% endif %}>5 звезд</option>
<option value="4" {% if rating_filter == '4' %}selected{% endif %}>4 звезды</option>
<option value="3" {% if rating_filter == '3' %}selected{% endif %}>3 звезды</option>
<option value="2" {% if rating_filter == '2' %}selected{% endif %}>2 звезды</option>
<option value="1" {% if rating_filter == '1' %}selected{% endif %}>1 звезда</option>
</select>
</div>
</div>
{% if reviews %}
{% for review in reviews %}
<div class="review-card">
<div class="review-header">
<strong>{{ review.username }}</strong>
<div class="rating">
{% for i in range(5) %}
{% if i < review.rating %}
★
{% else %}
☆
{% endif %}
{% endfor %}
</div>
</div>
<div class="review-date">{{ review.created_at }}</div>
<div class="review-comment">{{ review.comment }}</div>
</div>
{% endfor %}
{% else %}
<div class="empty-reviews">
<p>😊 Пока нет отзывов. Будьте первым!</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}

15
templates/services.html Normal file
View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h2>Наши услуги</h2>
<div class="services-list">
{% for service in services %}
<div class="service-item">
<h3>{{ service.name }}</h3>
<p>{{ service.description }}</p>
<p class="price">{{ service.price }} рублей</p>
<p class="duration">Длительность: {{ service.duration }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

249
templates/users.html Normal file
View file

@ -0,0 +1,249 @@
{% extends "base.html" %}
{% block content %}
<h2>Управление пользователями</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="filters">
<h3>Фильтры и сортировка</h3>
<div class="filter-bar">
<label>Сортировка:</label>
<select onchange="window.location.href='?sort='+this.value+'&role={{ role }}'">
<option value="newest" {% if sort == 'newest' %}selected{% endif %}>Новые сначала</option>
<option value="oldest" {% if sort == 'oldest' %}selected{% endif %}>Старые сначала</option>
<option value="name_asc" {% if sort == 'name_asc' %}selected{% endif %}>Имя (А-Я)</option>
<option value="name_desc" {% if sort == 'name_desc' %}selected{% endif %}>Имя (Я-А)</option>
</select>
<label>Роль:</label>
<select onchange="window.location.href='?sort={{ sort }}&role='+this.value">
<option value="all" {% if role == 'all' %}selected{% endif %}>Все</option>
<option value="admin" {% if role == 'admin' %}selected{% endif %}>Администраторы</option>
<option value="user" {% if role == 'user' %}selected{% endif %}>Пользователи</option>
</select>
</div>
</div>
<div class="admin-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Имя пользователя</th>
<th>Email</th>
<th>Роль</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td><strong>{{ user.username }}</strong></td>
<td>{{ user.email or '-' }}</td>
<td>
<span class="role-badge role-{{ user.role }}">
{% if user.role == 'admin' %}👑 Администратор
{% else %}👤 Пользователь
{% endif %}
</span>
</td>
<td>{{ user.created_at[:16] }}</td>
<td>
{% if user.role != 'admin' %}
<form method="POST" action="{{ url_for('make_admin', user_id=user.id) }}?sort={{ sort }}&role={{ role }}" style="display: inline;">
<button type="submit" class="btn btn-warning btn-sm">👑 Сделать админом</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_user', user_id=user.id) }}?sort={{ sort }}&role={{ role }}" style="display: inline;" onsubmit="return confirm('Удалить пользователя {{ user.username }}? Все его заказы и отзывы также будут удалены.')">
<button type="submit" class="btn btn-danger btn-sm">🗑️ Удалить</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not users %}
<div class="empty-state">
<p>Нет пользователей для отображения</p>
</div>
{% endif %}
</div>
<style>
.filters {
background: white;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.filters h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #4a2c2c;
font-size: 1.2rem;
}
.filter-bar {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
align-items: center;
}
.filter-bar label {
font-weight: bold;
color: #4a2c2c;
}
.filter-bar select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
}
.admin-table {
overflow-x: auto;
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.admin-table table {
width: 100%;
border-collapse: collapse;
}
.admin-table th,
.admin-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.admin-table th {
background-color: #4a2c2c;
color: white;
font-weight: bold;
}
.admin-table tr:hover {
background-color: #f5f5f5;
}
.role-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: bold;
}
.role-admin {
background-color: #dc3545;
color: white;
}
.role-user {
background-color: #28a745;
color: white;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
display: inline-block;
margin: 0.25rem;
text-decoration: none;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-warning {
background-color: #ffc107;
color: #333;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #666;
}
.alert {
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
@media (max-width: 768px) {
.filter-bar {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.filter-bar select {
width: 100%;
}
.admin-table th,
.admin-table td {
padding: 0.5rem;
font-size: 0.85rem;
}
.btn-sm {
display: block;
width: 100%;
margin: 0.25rem 0;
}
}
</style>
{% endblock %}