commit 6f573999ef582fddb9c606879d515cb58e6adfa9 Author: kosarewmihail Date: Sat Apr 4 08:43:12 2026 +0500 initial commit diff --git a/app.py b/app.py new file mode 100644 index 0000000..65c15f2 --- /dev/null +++ b/app.py @@ -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/') +@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//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//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//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//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//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//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) diff --git a/create_tables.py b/create_tables.py new file mode 100644 index 0000000..ef86392 --- /dev/null +++ b/create_tables.py @@ -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() diff --git a/horse_farm.db b/horse_farm.db new file mode 100644 index 0000000..9c472f0 Binary files /dev/null and b/horse_farm.db differ diff --git a/load_date.py b/load_date.py new file mode 100644 index 0000000..518eb5f --- /dev/null +++ b/load_date.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cbfe3b4 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/resource/abonements.xlsx b/resource/abonements.xlsx new file mode 100644 index 0000000..02bbe6d Binary files /dev/null and b/resource/abonements.xlsx differ diff --git a/resource/certificates.xlsx b/resource/certificates.xlsx new file mode 100644 index 0000000..fd77233 Binary files /dev/null and b/resource/certificates.xlsx differ diff --git a/resource/services.xlsx b/resource/services.xlsx new file mode 100644 index 0000000..c5514c2 Binary files /dev/null and b/resource/services.xlsx differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..eb3c3ee --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,1151 @@ +/* ========== ОСНОВНЫЕ СТИЛИ ========== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-dark: #2c1810; + --primary: #8B5A2B; + --primary-light: #c49a6c; + --accent: #d4a373; + --light: #fef9f0; + --dark: #2c2c2c; + --gray: #6c757d; + --light-gray: #e9ecef; + --success: #28a745; + --warning: #ffc107; + --danger: #dc3545; + --info: #17a2b8; + --shadow-sm: 0 2px 4px rgba(0,0,0,0.1); + --shadow-md: 0 4px 6px rgba(0,0,0,0.1); + --shadow-lg: 0 10px 15px rgba(0,0,0,0.1); +} + +body { + font-family: 'Montserrat', 'Arial', sans-serif; + line-height: 1.6; + color: #2c2c2c; + background-image: url('/static/images/background.jpg'); + background-size: cover; + background-position: center; + background-attachment: fixed; + background-repeat: no-repeat; + position: relative; +} + +/* Затемнение фона */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: -1; + pointer-events: none; +} + +html, body { + height: 100%; +} + +.wrapper { + min-height: 100%; + display: flex; + flex-direction: column; +} + +/* ========== СТИЛИ ДЛЯ ВСЕХ БЛОКОВ (КАК НА ГЛАВНОЙ) ========== */ + +/* Все блоки - темный полупрозрачный фон, как у hero */ +section, +.service-item, +.abonement-item, +.certificate-item, +.discount-card, +.review-card, +.order-card, +.auth-container, +.order-form, +.review-form, +.chat-container, +.admin-card, +.filters, +.about-content, +.about-hero, +.about-quote, +.gallery-item, +.advantages .advantage-item, +.add-review, +.reviews-list, +.chat-messages, +.chat-input, +.orders-filters, +.orders-list, +.admin-table, +.gallery-intro, +.gallery-note, +.discounts-intro, +.discounts-note, +.empty-orders, +.empty-reviews, +.empty-state { + background: rgba(0, 0, 0, 0.55) !important; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: var(--shadow-sm); + border-radius: 20px; + transition: transform 0.2s ease; +} + +/* Убираем смену цвета при наведении - только легкое поднятие */ +section:hover, +.service-item:hover, +.abonement-item:hover, +.certificate-item:hover, +.discount-card:hover, +.review-card:hover, +.order-card:hover, +.gallery-item:hover, +.advantages .advantage-item:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + background: rgba(0, 0, 0, 0.58) !important; +} + +/* Весь текст внутри блоков делаем белым */ +section p, +.service-item p, +.abonement-item p, +.certificate-item p, +.discount-card p, +.review-card p, +.order-card p, +.auth-container p, +.order-form p, +.review-form p, +.chat-container p, +.admin-card p, +.filters p, +.about-content p, +.about-hero p, +.about-quote p, +.gallery-item p, +.advantages .advantage-item p, +.add-review p, +.reviews-list p, +.chat-messages p, +.chat-input p, +.orders-filters p, +.orders-list p, +.admin-table p, +.gallery-intro p, +.gallery-note p, +.discounts-intro p, +.discounts-note p, +.empty-orders p, +.empty-reviews p, +.empty-state p, +section li, +.service-item li, +.about-content li, +.review-comment, +.order-details p { + color: #ffffff !important; + text-shadow: 1px 1px 2px rgba(0,0,0,0.3); +} + +/* Заголовки внутри блоков делаем светлыми */ +section h2, +section h3, +.service-item h3, +.abonement-item h3, +.certificate-item h3, +.discount-card h3, +.review-card h3, +.order-card h3, +.auth-container h2, +.auth-container h3, +.order-form h2, +.order-form h3, +.review-form h2, +.review-form h3, +.chat-container h3, +.admin-card h3, +.filters h3, +.about-content h3, +.about-hero h3, +.about-quote h3, +.gallery-item h3, +.advantages .advantage-item h4, +.add-review h3, +.reviews-list h3, +.gallery-intro h3, +.discounts-intro h3, +.discounts-note h3, +.empty-orders h3, +.empty-reviews h3, +.empty-state h3 { + color: #f5cba0 !important; +} + +/* Специально для цен - делаем золотыми */ +.price { + color: #f5cba0 !important; + font-size: 1.4rem; + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0,0,0,0.3); +} + +/* Рейтинг звезд - оставляем желтым */ +.rating { + color: #ffc107 !important; +} + +/* Ссылки внутри блоков */ +section a, +.service-item a, +.order-card a { + color: #f5cba0 !important; +} + +section a:hover, +.service-item a:hover, +.order-card a:hover { + color: #ffdd99 !important; +} + +/* Статусы заказов */ +.status-pending { + background: #ffc107; + color: #856404; +} + +.status-confirmed { + background: #17a2b8; + color: white; +} + +.status-completed { + background: #28a745; + color: white; +} + +.status-cancelled { + background: #dc3545; + color: white; +} + +/* Шапка сайта */ +header { + background: rgba(44, 24, 16, 0.92) !important; + 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; + margin-bottom: 1rem; + color: #fff; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +/* Навигация */ +nav ul { + list-style: none; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; +} + +nav a { + color: #fff; + text-decoration: none; + padding: 0.6rem 1.2rem; + border-radius: 50px; + transition: all 0.2s ease; + font-weight: 500; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +nav a:hover { + background: #d4a373; + color: #2c1810; + transform: translateY(-2px); +} + +/* Основной контент */ +.content { + flex: 1 1 auto; + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; + width: 100%; +} + +/* Заголовки страниц (h2 вне блоков) */ +.content > h2, +.orders-header h2, +.admin-dashboard h2 { + background: rgba(0, 0, 0, 0.55) !important; + backdrop-filter: blur(4px); + display: inline-block; + padding: 0.6rem 1.2rem; + border-radius: 12px; + border-left: 4px solid #f5cba0; + margin-bottom: 1.5rem; + color: white !important; +} + +/* Главная страница (hero уже темный, оставляем как есть) */ +.hero { + text-align: center; + padding: 3rem 2rem; + background: rgba(0, 0, 0, 0.55) !important; + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.hero h2 { + color: white !important; + font-size: 2.5rem; + border-left: none; + padding-left: 0; +} + +.about-text { + max-width: 800px; + margin: 1.5rem auto; + font-size: 1.1rem; + line-height: 1.6; +} + +.about-text p { + color: white !important; + text-shadow: 1px 1px 2px rgba(0,0,0,0.3); +} + +/* Карточки */ +.services-list, .abonements-list, .certificates-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-top: 1rem; +} + +.service-item, .abonement-item, .certificate-item { + padding: 1.5rem; +} + +.duration { + color: #d4a373 !important; + font-style: italic; + font-size: 0.9rem; +} + +/* Преимущества */ +.advantages-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-top: 1rem; +} + +.advantage-item { + padding: 1.5rem; + text-align: center; +} + +/* Скидки */ +.discounts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin: 1.5rem 0; +} + +.discount-card { + padding: 1.5rem; + text-align: center; +} + +.discount-icon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.discount-value { + font-size: 1.5rem; + font-weight: bold; + color: #f5cba0 !important; + margin: 0.5rem 0; +} + +/* Галерея */ +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; + padding: 0.5rem; +} + +.gallery-item { + overflow: hidden; +} + +.gallery-item img { + width: 100%; + height: 200px; + object-fit: cover; + transition: transform 0.3s ease; +} + +.gallery-item:hover img { + transform: scale(1.05); +} + +.gallery-caption { + padding: 0.75rem; + text-align: center; + font-weight: 500; + color: #f5cba0 !important; +} + +/* Формы */ +.auth-container, .order-form, .review-form { + max-width: 500px; + margin: 2rem auto; + padding: 2rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.3rem; + font-weight: 600; + color: #f5cba0 !important; +} + +.form-group input, +.form-group select, +.form-group textarea { + 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, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #f5cba0; + box-shadow: 0 0 0 3px rgba(245, 203, 160, 0.2); +} + +/* Кнопки */ +.btn { + display: inline-block; + padding: 0.6rem 1.2rem; + border: none; + border-radius: 50px; + cursor: pointer; + text-decoration: none; + font-size: 1rem; + font-weight: 600; + transition: all 0.2s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%) !important; + color: #2c1810 !important; + font-weight: bold; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #ffdd99 0%, #f5cba0 100%) !important; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.btn-danger { + background-color: var(--danger); + color: white; +} + +.btn-danger:hover { + background-color: #c82333; +} + +.btn-warning { + background-color: var(--warning); + color: #333; +} + +.btn-warning:hover { + background-color: #e0a800; +} + +/* Заказы */ +.order-card { + padding: 1rem; + margin-bottom: 1rem; + overflow: hidden; +} + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.order-header h3 { + margin: 0; + font-size: 1.1rem; + color: #f5cba0 !important; +} + +.order-details p { + margin: 0.5rem 0; +} + +/* Отзывы */ +.reviews-section { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; +} + +.add-review, .reviews-list { + padding: 1.5rem; +} + +.review-card { + padding: 1rem; + margin-bottom: 1rem; +} + +.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 !important; + font-weight: 600; +} + +.review-date { + color: #d4a373 !important; + font-size: 0.7rem; + margin-bottom: 0.5rem; +} + +/* Чат */ +.chat-container { + overflow: hidden; +} + +.chat-messages { + height: 400px; + overflow-y: auto; + padding: 1rem; +} + +.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%) !important; + color: #2c1810 !important; + margin-left: auto; + text-align: right; +} + +.message-admin { + background: rgba(44, 24, 16, 0.85) !important; + color: white; +} + +.message-system { + background: rgba(255, 255, 255, 0.2) !important; + color: #f5cba0 !important; + text-align: center; + max-width: 100%; + font-style: italic; +} + +.message-time { + color: rgba(255,255,255,0.7); + font-size: 0.7rem; +} + +.chat-input { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Админ панель */ +.admin-dashboard { + padding: 1rem; +} + +.admin-menu { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.admin-card { + padding: 1.5rem; + text-align: center; +} + +.admin-table { + overflow-x: auto; + padding: 0.5rem; +} + +.admin-table table { + width: 100%; + border-collapse: collapse; +} + +.admin-table th, +.admin-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.admin-table th { + background: rgba(44, 24, 16, 0.9) !important; + color: #f5cba0; + font-weight: 600; +} + +.admin-table td { + color: white !important; +} + +.admin-table tr:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* Фильтры */ +.filter-bar { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: center; + margin-top: 0.5rem; +} + +.filter-bar label { + font-size: 0.85rem; + font-weight: 500; + color: #f5cba0 !important; +} + +.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; +} + +/* Уведомления */ +.alert { + padding: 0.75rem; + margin-bottom: 1rem; + border-radius: 8px; +} + +.alert-success { + background: rgba(40, 167, 69, 0.9) !important; + color: white; + border: 1px solid #c3e6cb; +} + +.alert-danger { + background: rgba(220, 53, 69, 0.9) !important; + color: white; + border: 1px solid #f5c6cb; +} + +.alert-warning { + background: rgba(255, 193, 7, 0.9) !important; + color: #2c2c2c; + border: 1px solid #ffeeba; +} + +/* Специальное предложение */ +.special-offer { + background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%) !important; + border-radius: 20px; + padding: 1.5rem; + text-align: center; + margin: 1.5rem 0; +} + +.special-offer h3, +.special-offer p { + color: #2c1810 !important; + text-shadow: none; +} + +/* Футер */ +footer { + background: rgba(30, 16, 10, 0.98) !important; + border-top: 3px solid #d4a373; + padding: 2rem 0 1rem 0; + margin-top: auto; + box-shadow: 0 -5px 20px rgba(0,0,0,0.2); +} + +.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 !important; + font-size: 1.25rem; + font-weight: 700; + margin-bottom: 1rem; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + letter-spacing: 1px; + position: relative; + display: inline-block; +} + +.footer-section h4::after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + width: 40px; + height: 2px; + background: #d4a373; + border-radius: 2px; +} + +.footer-section p { + color: #f0e2d0 !important; + font-size: 0.95rem; + line-height: 1.6; + margin: 0.6rem 0; + transition: all 0.2s ease; +} + +.footer-section p:hover { + color: #ffdd99 !important; + transform: translateX(5px); +} + +.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; +} + +.footer-bottom p { + color: #ddc8aa !important; + font-size: 0.9rem; + font-weight: 500; +} + +.team-credit { + font-size: 0.75rem; + margin-top: 0.6rem; + color: #c9a87c !important; + transition: all 0.2s ease; + cursor: default; +} + +.team-credit:hover { + color: #ffdd99 !important; + text-shadow: 0 0 5px rgba(212,163,115,0.5); +} + +/* ========== УМНЫЙ BLUR (только для мощных устройств) ========== */ +@media (min-width: 1200px) and (prefers-reduced-motion: no-preference) { + section, + .service-item, + .abonement-item, + .certificate-item, + .discount-card, + .review-card, + .order-card, + .auth-container, + .order-form, + .review-form, + .chat-container, + .admin-card, + .filters, + .about-content, + .about-hero, + .about-quote, + .gallery-item, + .advantages .advantage-item, + .add-review, + .reviews-list, + .gallery-intro, + .gallery-note, + .discounts-intro, + .discounts-note { + backdrop-filter: blur(6px); + transition: backdrop-filter 0.2s ease; + } + + section:hover, + .service-item:hover, + .discount-card:hover, + .review-card:hover, + .order-card:hover, + .gallery-item:hover { + backdrop-filter: blur(0px); + } + + header, footer { + backdrop-filter: blur(8px); + } + + .content > h2, + .orders-header h2, + .admin-dashboard h2 { + backdrop-filter: blur(4px); + } +} + +/* Адаптивность */ +@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; + } + + .hero h2 { + font-size: 1.8rem; + } + + .hero { + padding: 2rem 1rem; + } + + .reviews-section { + grid-template-columns: 1fr; + gap: 1rem; + } + + .message { + max-width: 95%; + } + + .footer-content { + grid-template-columns: 1fr; + text-align: center; + gap: 1rem; + } + + .footer-section h4::after { + left: 50%; + transform: translateX(-50%); + } + + .footer-section p:hover { + transform: translateX(0); + } +} +/* ========== ИСПРАВЛЕНИЕ РАЗМЕРОВ БЛОКОВ С ТЕКСТОМ ========== */ + +/* Все текстовые блоки вне карточек - делаем полноразмерными */ +.gallery-intro, +.gallery-note, +.discounts-intro, +.discounts-note, +.empty-orders, +.empty-reviews, +.empty-state, +.orders-stats { + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 0 0 1.5rem 0; + padding: 1.2rem 1.5rem; + background: rgba(0, 0, 0, 0.55) !important; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Текст внутри этих блоков */ +.gallery-intro p, +.gallery-note p, +.discounts-intro p, +.discounts-note p, +.empty-orders p, +.empty-reviews p, +.empty-state p { + color: white !important; + margin: 0; + line-height: 1.5; + word-wrap: break-word; +} + +/* Заголовки внутри этих блоков */ +.discounts-note h3, +.empty-orders h3, +.empty-reviews h3, +.empty-state h3 { + color: #f5cba0 !important; + margin-bottom: 0.8rem; + font-size: 1.2rem; +} + +/* Специальное предложение */ +.special-offer { + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 1.5rem 0; + padding: 1.5rem; + background: linear-gradient(135deg, #f5cba0 0%, #d4a373 100%) !important; + border-radius: 20px; + text-align: center; +} + +.special-offer h3, +.special-offer p { + color: #2c1810 !important; + margin: 0; +} + +.special-offer h3 { + margin-bottom: 0.8rem; +} + +/* Фильтры */ +.filters { + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin-bottom: 1.5rem; + padding: 1rem 1.5rem; + background: rgba(0, 0, 0, 0.55) !important; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.filters h3 { + color: #f5cba0 !important; + margin-bottom: 0.8rem; + font-size: 1.2rem; +} + +/* Статистика заказов */ +.orders-stats { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; + padding: 0.8rem 1.2rem; +} + +.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 !important; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Заголовки страниц */ +.content > h2, +.orders-header h2, +.admin-dashboard h2 { + width: auto; + max-width: 100%; + box-sizing: border-box; + display: inline-block; + margin-bottom: 1.5rem; + padding: 0.6rem 1.2rem; + background: rgba(0, 0, 0, 0.55) !important; + border-radius: 12px; + border-left: 4px solid #f5cba0; + color: white !important; + font-size: 1.8rem; +} + +/* Контейнер для всего контента - чтобы ничего не выходило за края */ +.content { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; + width: 100%; + overflow-x: hidden; +} + +/* Все блоки внутри контента */ +.content > * { + max-width: 100%; + box-sizing: border-box; +} + +/* Отзывы */ +.reviews-section { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; + width: 100%; +} + +.add-review, +.reviews-list { + width: 100%; + min-width: 0; + overflow: hidden; +} + +/* Адаптивность для мобильных */ +@media (max-width: 768px) { + .gallery-intro, + .gallery-note, + .discounts-intro, + .discounts-note, + .empty-orders, + .empty-reviews, + .empty-state, + .filters, + .orders-stats { + padding: 0.8rem 1rem; + } + + .content > h2, + .orders-header h2, + .admin-dashboard h2 { + font-size: 1.3rem; + padding: 0.4rem 0.8rem; + } + + .special-offer { + padding: 1rem; + } + + .special-offer h3 { + font-size: 1.1rem; + } + + .special-offer p { + font-size: 0.9rem; + } + + .reviews-section { + gap: 1rem; + } +} +/* ========== ИСПРАВЛЕНИЯ ========== */ + +/* Цвет заголовка "О нашей базе" на белый */ +.about-hero h2 { + color: white !important; +} + +/* Блок специального предложения - такой же как у остальных */ +.special-offer { + background: rgba(0, 0, 0, 0.55) !important; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + padding: 1.5rem; + text-align: center; + margin: 1.5rem 0; + backdrop-filter: blur(4px); +} + +.special-offer h3, +.special-offer p { + color: white !important; +} + +.special-offer h3 { + color: #f5cba0 !important; + margin-bottom: 0.8rem; +} + +/* Отступ сверху для пустого состояния отзывов */ +.empty-reviews { + margin-top: 1rem !important; +} + +/* Статичное поле комментария - фиксированная высота */ +.form-group textarea { + resize: none; + height: 100px; +} + +/* Убираем смайлики в фильтрах заказов */ +.filter-bar label { + font-size: 0.85rem; + font-weight: 500; + color: #f5cba0 !important; +} + +.filter-bar label::before { + display: none; +} + +/* Приводим фильтры к единому стилю */ +.orders-filters .filter-bar label, +.filters .filter-bar label { + background: none; + padding: 0; +} +/* Белый цвет для подсказок */ +small { + color: #ffffff !important; + opacity: 0.8; +} diff --git a/static/images/background.jpg b/static/images/background.jpg new file mode 100644 index 0000000..c268c07 Binary files /dev/null and b/static/images/background.jpg differ diff --git a/static/images/gallery/зимние сани.jpg b/static/images/gallery/зимние сани.jpg new file mode 100644 index 0000000..6116ae1 Binary files /dev/null and b/static/images/gallery/зимние сани.jpg differ diff --git a/static/images/gallery/катание на санях.jpg b/static/images/gallery/катание на санях.jpg new file mode 100644 index 0000000..055d33b Binary files /dev/null and b/static/images/gallery/катание на санях.jpg differ diff --git a/static/images/gallery/прогулка по лесу.jpg b/static/images/gallery/прогулка по лесу.jpg new file mode 100644 index 0000000..7ca6734 Binary files /dev/null and b/static/images/gallery/прогулка по лесу.jpg differ diff --git a/static/images/gallery/прогулка.jpg b/static/images/gallery/прогулка.jpg new file mode 100644 index 0000000..fe13246 Binary files /dev/null and b/static/images/gallery/прогулка.jpg differ diff --git a/static/images/gallery/прогулочка.jpg b/static/images/gallery/прогулочка.jpg new file mode 100644 index 0000000..4c4c188 Binary files /dev/null and b/static/images/gallery/прогулочка.jpg differ diff --git a/templates/abonements.html b/templates/abonements.html new file mode 100644 index 0000000..8b1e611 --- /dev/null +++ b/templates/abonements.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +

Абонементы

+
+ {% for abonement in abonements %} +
+

{{ abonement.name }}

+

{{ abonement.description }}

+

{{ abonement.price }} рублей

+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..9588fcc --- /dev/null +++ b/templates/about.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

🐴 О нашей базе "Конный двор"

+

Место, где лошади и люди находят друг друга

+
+ +
+

Кто мы такие?

+

"Конный двор" — это уютная конно-спортивная база, расположенная в живописном месте Республики Башкортостан. Мы создали это место для тех, кто хочет прикоснуться к миру лошадей, научиться верховой езде или просто отдохнуть на природе в компании этих удивительных животных.

+ +

Наша миссия

+

Мы стремимся подарить каждому посетителю радость общения с лошадьми, помочь раскрыть красоту и пользу верховой езды, создать комфортные условия для занятий и прогулок. Наши инструкторы — профессионалы с многолетним опытом, которые найдут подход как к взрослым, так и к детям.

+ +

Что мы предлагаем?

+
    +
  • Обучение верховой езде для начинающих и опытных всадников
  • +
  • Конные прогулки по живописному лесу
  • +
  • Зимние прогулки в санях
  • +
  • Экскурсии с рассказом о лошадях и уходе за ними
  • +
  • Подарочные сертификаты и абонементы
  • +
+ +

Почему выбирают нас?

+
    +
  • Опытные инструкторы с педагогическим образованием
  • +
  • Добрые, ухоженные и обученные лошади
  • +
  • Живописные маршруты для прогулок
  • +
  • Индивидуальный подход к каждому ученику
  • +
  • Доступные цены и гибкая система скидок
  • +
+
+ +
+

Лошадь — это зеркало души человека. Приходя к нам, вы не просто учитесь ездить верхом, вы учитесь понимать, чувствовать и доверять. И это меняет жизнь.

+
— Основатель базы "Конный двор"
+
+{% endblock %} diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..3b304e4 --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +

Админ панель

+ +
+
+
+

📦 Заказы

+

Управление заказами пользователей

+ Перейти +
+ +
+

💬 Отзывы

+

Модерация отзывов

+ Перейти +
+ +
+

👥 Пользователи

+

Управление пользователями

+ Перейти +
+
+
+{% endblock %} diff --git a/templates/admin/messages.html b/templates/admin/messages.html new file mode 100644 index 0000000..88850c1 --- /dev/null +++ b/templates/admin/messages.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

Новые сообщения от пользователей

+ +{% if messages %} +
+ {% for msg in messages %} +
+
+ Заказ #{{ msg.order_id }} + От: {{ msg.user_name }} + {{ msg.created_at }} +
+
{{ msg.message }}
+ +
+ {% endfor %} +
+{% else %} +

Новых сообщений нет.

+{% endif %} +{% endblock %} diff --git a/templates/admin/orders.html b/templates/admin/orders.html new file mode 100644 index 0000000..cca8c77 --- /dev/null +++ b/templates/admin/orders.html @@ -0,0 +1,170 @@ +{% extends "base.html" %} + +{% block content %} +

Управление заказами

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+

Фильтры и сортировка

+
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + + + {% endfor %} + +
IDПользовательУслугаДатаВремяЦенаСкидкаОплатаСтатусСообщенияДействия
{{ order.id }}{{ order.username or 'Гость' }}{{ order.service_name }}{{ order.date or '-' }}{{ order.time or '-' }}{{ order.price }} руб. + {% if order.discount_applied %} + {{ order.discount_amount }} руб. + {% else %} + — + {% endif %} + + {% if order.payment_status == 'paid' %} + ✅ Оплачено + {% else %} + ⚠️ Не оплачено + {% endif %} + + + + {% if order.status == 'pending' %}Ожидает + {% elif order.status == 'confirmed' %}Подтвержден + {% elif order.status == 'completed' %}Выполнен + {% elif order.status == 'cancelled' %}Отменен + {% endif %} + + + {% if order.unread_count > 0 %} + {{ order.unread_count }} + {% else %} + — + {% endif %} + + Чат +
+ +
+
+ +
+
+
+ + +{% endblock %} diff --git a/templates/admin/reviews.html b/templates/admin/reviews.html new file mode 100644 index 0000000..ddbc64b --- /dev/null +++ b/templates/admin/reviews.html @@ -0,0 +1,314 @@ +{% extends "base.html" %} + +{% block content %} +

Модерация отзывов

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+

Фильтры и сортировка

+
+ + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + {% for review in reviews %} + + + + + + + + + + + {% endfor %} + +
IDПользовательЗаказОценкаОтзывСтатусДатаДействия
{{ review.id }}{{ review.username }}Заказ #{{ review.order_id }} +
+ {% for i in range(5) %} + {% if i < review.rating %} + ★ + {% else %} + ☆ + {% endif %} + {% endfor %} +
+
+
{{ review.comment }}
+
+ + {% if review.status == 'pending' %}На модерации + {% elif review.status == 'approved' %}Одобрен + {% elif review.status == 'rejected' %}Отклонен + {% endif %} + + {{ review.created_at[:16] }} +
+ +
+
+ +
+
+ + {% if not reviews %} +
+

Нет отзывов для отображения

+
+ {% endif %} +
+ + +{% endblock %} diff --git a/templates/admin/users.html b/templates/admin/users.html new file mode 100644 index 0000000..18f67d8 --- /dev/null +++ b/templates/admin/users.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% block content %} +

Управление пользователями

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDИмя пользователяEmailРольДата регистрацииДействия
{{ user.id }}{{ user.username }}{{ user.email or '-' }} + {% if user.role == 'admin' %} + Админ + {% else %} + Пользователь + {% endif %} + {{ user.created_at[:10] }} + {% if user.role != 'admin' %} +
+ +
+ {% endif %} +
+ +
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..024e7f6 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,137 @@ + + + + + + Конный двор - Конно-спортивная база + + + + + +
+
+

🐎 Конно-спортивная база "Конный двор" 🐎

+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ +
+ + +
+
+ + + + + + diff --git a/templates/certificates.html b/templates/certificates.html new file mode 100644 index 0000000..a155164 --- /dev/null +++ b/templates/certificates.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +

Подарочные сертификаты

+
+ {% for certificate in certificates %} +
+

{{ certificate.name }}

+

{{ certificate.description }}

+

{{ certificate.price }} рублей

+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/chat.html b/templates/chat.html new file mode 100644 index 0000000..267ef41 --- /dev/null +++ b/templates/chat.html @@ -0,0 +1,237 @@ +{% extends "base.html" %} + +{% block content %} + + +

Чат по заказу #{{ order.id }}

+ +
+
+

Информация о заказе

+

Услуга: {{ order.service_name }}

+

Дата: {{ order.date }} в {{ order.time }}

+

Количество человек: {{ order.persons }}

+

Цена: + {% if order.discount_applied %} + {{ order.original_price }} руб. + {{ order.price }} руб. + Скидка 15% + {% else %} + {{ order.price }} руб. + {% endif %} +

+

Статус: {{ order.status }}

+ {% if order.comment %} +

Ваш комментарий: {{ order.comment }}

+ {% endif %} +
+ +
+ {% if messages %} + {% for message in messages %} +
+
+ + {% if message.sender_type == 'system' %} + 📢 Система + {% elif message.sender_type == 'admin' %} + 👑 Администратор + {% else %} + 👤 {{ message.sender_name }} + {% endif %} + + {{ message.created_at }} +
+
{{ message.message }}
+
+ {% endfor %} + {% else %} +
+
Нет сообщений. Напишите что-нибудь...
+
+ {% endif %} +
+ +
+
+ + + +
+
+
+ + +{% endblock %} diff --git a/templates/discounts.html b/templates/discounts.html new file mode 100644 index 0000000..7a6380f --- /dev/null +++ b/templates/discounts.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +

Скидки и специальные предложения

+ +
+
+

Мы хотим, чтобы общение с лошадьми было доступным для всех! Поэтому у нас действует гибкая система скидок и специальных предложений.

+
+ +
+
+
🎂
+

Скидка в честь дня рождения

+
10%
+

При предъявлении документа, подтверждающего день рождения (скидка действует в день рождения и 3 дня после)

+
+ +
+
👶
+

Детям до 5 лет

+
Бесплатно
+

Прогулки в санях для детей до 5 лет - бесплатно в сопровождении родителей

+
+ +
+
👨‍👩‍👧‍👦
+

Семейные прогулки

+
15%
+

При заказе прогулки для семьи из 4-х человек и более

+
+ +
+
📅
+

Абонементы

+
Экономия до 20%
+

При покупке абонемента вы экономите до 20% от стоимости разовых занятий

+
+ +
+
🎓
+

Студентам и школьникам

+
10%
+

При предъявлении студенческого билета или справки из школы

+
+ +
+
👴
+

Пенсионерам

+
10%
+

При предъявлении пенсионного удостоверения

+
+
+ +
+

Важно!

+

Скидки не суммируются. Вы можете выбрать наиболее выгодное для вас предложение. + Для получения скидки необходимо предъявить подтверждающий документ перед началом прогулки или занятия.

+
+ +
+

Специальное предложение для именинников!

+

В свой день рождения вы можете прийти на прогулку с компанией до 3-х человек и получить скидку 10% для всей компании!

+
+
+{% endblock %} diff --git a/templates/gallery.html b/templates/gallery.html new file mode 100644 index 0000000..9598e07 --- /dev/null +++ b/templates/gallery.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +

Фотогалерея

+ + + + + +{% if images %} + +{% endif %} +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..deb8bc0 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

Добро пожаловать в "Конный двор"!

+
+

Это база где вы сможете поближе познакомиться с лошадьми, обучиться навыкам верховной езды, подышать свежим воздухом.

+

Наша база занимается проведением прогулок верхом и в санях, а также обучением навыкам верховной езды. Приходите к нам и вы откроете для себя удивительный мир лошадей и отдыха!

+
+
+{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..9a6cab3 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block content %} +
+

Вход в систему

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ + +
+ + +
+ + +
+{% endblock %} diff --git a/templates/my_orders.html b/templates/my_orders.html new file mode 100644 index 0000000..6c5f1f3 --- /dev/null +++ b/templates/my_orders.html @@ -0,0 +1,281 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

Мои заказы

+
+ {% 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 %} + Всего: {{ total }} + Ожидают: {{ pending }} + Подтверждены: {{ confirmed }} + Выполнены: {{ completed }} +
+
+ +
+
+ + + + +
+
+ +{% if orders %} +
+ {% for order in orders %} +
+
+

Заказ #{{ order.id }}

+ + {% if order.status == 'pending' %}Ожидает + {% elif order.status == 'confirmed' %}Подтвержден + {% elif order.status == 'completed' %}Выполнен + {% elif order.status == 'cancelled' %}Отменен + {% endif %} + +
+
+

Услуга: {{ order.service_name }}

+ {% if order.date and order.time %} +

Дата: {{ order.date }} в {{ order.time }}

+ {% endif %} +

Количество человек: {{ order.persons }}

+

Цена: + {% if order.discount_applied %} + {{ order.original_price }} руб. + {{ order.price }} руб. + Скидка {{ order.discount_amount }} руб. (15%) + {% else %} + {{ order.price }} руб. + {% endif %} +

+ +
+

💳 Оплата: + {% if order.payment_status == 'paid' %} + ✅ Оплачено + {% else %} + ⚠️ Не оплачено + {% endif %} +

+

📞 Реквизиты для оплаты: +7 962 545 066 (СБП, перевод по номеру телефона)

+

💵 Наличные: на месте встречи

+

Оплата производится при получении услуги или заранее по номеру телефона. После оплаты администратор подтвердит статус.

+
+ + {% if order.comment %} +

Комментарий: {{ order.comment }}

+ {% endif %} +

Дата заказа: {{ order.created_at }}

+ +
+
+ {% endfor %} +
+{% else %} +
+
🐴
+

У вас пока нет заказов

+

Хотите прокатиться верхом или обучиться верховой езде?

+ Сделать первый заказ +
+{% endif %} +{% endblock %} diff --git a/templates/order_form.html b/templates/order_form.html new file mode 100644 index 0000000..935b1f3 --- /dev/null +++ b/templates/order_form.html @@ -0,0 +1,182 @@ +{% extends "base.html" %} + +{% block content %} +

Оформление заказа

+ + + +
+
+ + +
+ +
+ + +
+ +
+ +
0 руб.
+ + +
+ +
+ + + Нельзя выбрать прошедшую дату +
+ +
+ + + Режим работы: с 9:00 до 20:00 +
+ +
+ + + от 1 до 20. При заказе прогулки для 4+ человек - скидка 15% +
+ +
+ + +
+ + +
+ + +{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..209714c --- /dev/null +++ b/templates/register.html @@ -0,0 +1,120 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

Регистрация

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ + + Формат: name@domain.ru +
+ +
+ + +
+ + +
+ + +
+{% endblock %} diff --git a/templates/reviews.html b/templates/reviews.html new file mode 100644 index 0000000..03b77fb --- /dev/null +++ b/templates/reviews.html @@ -0,0 +1,297 @@ +{% extends "base.html" %} + +{% block content %} + + +

Отзывы наших посетителей

+ +
+ +
+

Оставить отзыв

+ {% if session.user_id %} + {% if completed_orders %} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ {% else %} +

У вас нет выполненных заказов, на которые можно оставить отзыв.

+ {% endif %} + {% else %} +

Чтобы оставить отзыв, пожалуйста, войдите в систему.

+ {% endif %} +
+ + +
+
+

Отзывы

+
+ + + + + +
+
+ + {% if reviews %} + {% for review in reviews %} +
+
+ {{ review.username }} +
+ {% for i in range(5) %} + {% if i < review.rating %} + ★ + {% else %} + ☆ + {% endif %} + {% endfor %} +
+
+
{{ review.created_at }}
+
{{ review.comment }}
+
+ {% endfor %} + {% else %} +
+

😊 Пока нет отзывов. Будьте первым!

+
+ {% endif %} +
+
+{% endblock %} diff --git a/templates/services.html b/templates/services.html new file mode 100644 index 0000000..5e16db4 --- /dev/null +++ b/templates/services.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} +

Наши услуги

+
+ {% for service in services %} +
+

{{ service.name }}

+

{{ service.description }}

+

{{ service.price }} рублей

+

Длительность: {{ service.duration }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..21574cb --- /dev/null +++ b/templates/users.html @@ -0,0 +1,249 @@ +{% extends "base.html" %} + +{% block content %} +

Управление пользователями

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+

Фильтры и сортировка

+
+ + + + + +
+
+ +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDИмя пользователяEmailРольДата регистрацииДействия
{{ user.id }}{{ user.username }}{{ user.email or '-' }} + + {% if user.role == 'admin' %}👑 Администратор + {% else %}👤 Пользователь + {% endif %} + + {{ user.created_at[:16] }} + {% if user.role != 'admin' %} +
+ +
+ {% endif %} +
+ +
+
+ + {% if not users %} +
+

Нет пользователей для отображения

+
+ {% endif %} +
+ + +{% endblock %}