refactor: Extract admin blueprint Part 1 (Phase 6)

Moved 19 admin routes to blueprints/admin/:
- Recommendations: 3 routes (list, approve, reject)
- Users: 8 routes (list, add, toggle-admin, toggle-verified, update, assign-company, delete, reset-password)
- Fees: 5 routes (list, generate, mark-paid, bulk-mark-paid, export)
- Calendar admin: 3 routes (list, new, delete)

Note: AI-parse routes (/api/admin/users/ai-parse, ai-create) remain in app.py.

Aliases created for backward compatibility.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-31 08:05:06 +01:00
parent 55718ed211
commit 44268c5425
4 changed files with 943 additions and 60 deletions

123
app.py
View File

@ -1090,11 +1090,12 @@ def health_full():
# See: blueprints/public/routes.py
# ============================================================
# RECOMMENDATIONS ADMIN ROUTES
# MOVED TO: blueprints/admin/routes.py
# ============================================================
@app.route('/admin/recommendations')
@login_required
def admin_recommendations():
# @app.route('/admin/recommendations') # MOVED TO admin.admin_recommendations
# @login_required
def _old_admin_recommendations():
"""Admin panel for recommendations moderation"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
@ -1137,9 +1138,9 @@ def admin_recommendations():
db.close()
@app.route('/admin/recommendations/<int:recommendation_id>/approve', methods=['POST'])
@login_required
def admin_recommendation_approve(recommendation_id):
# @app.route('/admin/recommendations/<int:recommendation_id>/approve', methods=['POST']) # MOVED TO admin.admin_recommendation_approve
# @login_required
def _old_admin_recommendation_approve(recommendation_id):
"""Approve a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1168,9 +1169,9 @@ def admin_recommendation_approve(recommendation_id):
db.close()
@app.route('/admin/recommendations/<int:recommendation_id>/reject', methods=['POST'])
@login_required
def admin_recommendation_reject(recommendation_id):
# @app.route('/admin/recommendations/<int:recommendation_id>/reject', methods=['POST']) # MOVED TO admin.admin_recommendation_reject
# @login_required
def _old_admin_recommendation_reject(recommendation_id):
"""Reject a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1204,11 +1205,13 @@ def admin_recommendation_reject(recommendation_id):
# ============================================================
# USER MANAGEMENT ADMIN ROUTES
# MOVED TO: blueprints/admin/routes.py
# NOTE: AI-parse routes remain here (lines 1308-1591)
# ============================================================
@app.route('/admin/users')
@login_required
def admin_users():
# @app.route('/admin/users') # MOVED TO admin.admin_users
# @login_required
def _old_admin_users():
"""Admin panel for user management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
@ -1243,9 +1246,9 @@ def admin_users():
db.close()
@app.route('/admin/users/add', methods=['POST'])
@login_required
def admin_user_add():
# @app.route('/admin/users/add', methods=['POST']) # MOVED TO admin.admin_user_add
# @login_required
def _old_admin_user_add():
"""Create a new user (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1591,9 +1594,9 @@ def admin_users_bulk_create():
db.close()
@app.route('/admin/users/<int:user_id>/toggle-admin', methods=['POST'])
@login_required
def admin_user_toggle_admin(user_id):
# @app.route('/admin/users/<int:user_id>/toggle-admin', methods=['POST']) # MOVED TO admin.admin_user_toggle_admin
# @login_required
def _old_admin_user_toggle_admin(user_id):
"""Toggle admin status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1621,9 +1624,9 @@ def admin_user_toggle_admin(user_id):
db.close()
@app.route('/admin/users/<int:user_id>/toggle-verified', methods=['POST'])
@login_required
def admin_user_toggle_verified(user_id):
# @app.route('/admin/users/<int:user_id>/toggle-verified', methods=['POST']) # MOVED TO admin.admin_user_toggle_verified
# @login_required
def _old_admin_user_toggle_verified(user_id):
"""Toggle verified status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1652,9 +1655,9 @@ def admin_user_toggle_verified(user_id):
db.close()
@app.route('/admin/users/<int:user_id>/update', methods=['POST'])
@login_required
def admin_user_update(user_id):
# @app.route('/admin/users/<int:user_id>/update', methods=['POST']) # MOVED TO admin.admin_user_update
# @login_required
def _old_admin_user_update(user_id):
"""Update user data (name, email)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1707,9 +1710,9 @@ def admin_user_update(user_id):
db.close()
@app.route('/admin/users/<int:user_id>/assign-company', methods=['POST'])
@login_required
def admin_user_assign_company(user_id):
# @app.route('/admin/users/<int:user_id>/assign-company', methods=['POST']) # MOVED TO admin.admin_user_assign_company
# @login_required
def _old_admin_user_assign_company(user_id):
"""Assign a company to a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1746,9 +1749,9 @@ def admin_user_assign_company(user_id):
db.close()
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
@login_required
def admin_user_delete(user_id):
# @app.route('/admin/users/<int:user_id>/delete', methods=['POST']) # MOVED TO admin.admin_user_delete
# @login_required
def _old_admin_user_delete(user_id):
"""Delete a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1776,9 +1779,9 @@ def admin_user_delete(user_id):
db.close()
@app.route('/admin/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
def admin_user_reset_password(user_id):
# @app.route('/admin/users/<int:user_id>/reset-password', methods=['POST']) # MOVED TO admin.admin_user_reset_password
# @login_required
def _old_admin_user_reset_password(user_id):
"""Generate password reset token for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
@ -1812,18 +1815,20 @@ def admin_user_reset_password(user_id):
# ============================================================
# MEMBERSHIP FEES ADMIN
# MOVED TO: blueprints/admin/routes.py
# ============================================================
MONTHS_PL = [
# MONTHS_PL constant moved to blueprint
_OLD_MONTHS_PL = [
(1, 'Styczen'), (2, 'Luty'), (3, 'Marzec'), (4, 'Kwiecien'),
(5, 'Maj'), (6, 'Czerwiec'), (7, 'Lipiec'), (8, 'Sierpien'),
(9, 'Wrzesien'), (10, 'Pazdziernik'), (11, 'Listopad'), (12, 'Grudzien')
]
@app.route('/admin/fees')
@login_required
def admin_fees():
# @app.route('/admin/fees') # MOVED TO admin.admin_fees
# @login_required
def _old_admin_fees():
"""Admin panel for membership fee management"""
if not current_user.is_admin:
flash('Brak uprawnien do tej strony.', 'error')
@ -1917,9 +1922,9 @@ def admin_fees():
db.close()
@app.route('/admin/fees/generate', methods=['POST'])
@login_required
def admin_fees_generate():
# @app.route('/admin/fees/generate', methods=['POST']) # MOVED TO admin.admin_fees_generate
# @login_required
def _old_admin_fees_generate():
"""Generate fee records for all companies for a given month"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
@ -1976,9 +1981,9 @@ def admin_fees_generate():
db.close()
@app.route('/admin/fees/<int:fee_id>/mark-paid', methods=['POST'])
@login_required
def admin_fees_mark_paid(fee_id):
# @app.route('/admin/fees/<int:fee_id>/mark-paid', methods=['POST']) # MOVED TO admin.admin_fees_mark_paid
# @login_required
def _old_admin_fees_mark_paid(fee_id):
"""Mark a fee as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
@ -2025,9 +2030,9 @@ def admin_fees_mark_paid(fee_id):
db.close()
@app.route('/admin/fees/bulk-mark-paid', methods=['POST'])
@login_required
def admin_fees_bulk_mark_paid():
# @app.route('/admin/fees/bulk-mark-paid', methods=['POST']) # MOVED TO admin.admin_fees_bulk_mark_paid
# @login_required
def _old_admin_fees_bulk_mark_paid():
"""Bulk mark fees as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
@ -2064,9 +2069,9 @@ def admin_fees_bulk_mark_paid():
db.close()
@app.route('/admin/fees/export')
@login_required
def admin_fees_export():
# @app.route('/admin/fees/export') # MOVED TO admin.admin_fees_export
# @login_required
def _old_admin_fees_export():
"""Export fees to CSV"""
if not current_user.is_admin:
flash('Brak uprawnien.', 'error')
@ -2130,10 +2135,10 @@ def admin_fees_export():
# ============================================================
# Routes: /kalendarz, /kalendarz/<id>, /kalendarz/<id>/rsvp
# Admin calendar routes remain here
@app.route('/admin/kalendarz')
@login_required
def admin_calendar():
# Admin calendar routes MOVED TO: blueprints/admin/routes.py
# @app.route('/admin/kalendarz') # MOVED TO admin.admin_calendar
# @login_required
def _old_admin_calendar():
"""Panel admin - zarządzanie wydarzeniami"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
@ -2147,9 +2152,9 @@ def admin_calendar():
db.close()
@app.route('/admin/kalendarz/nowy', methods=['GET', 'POST'])
@login_required
def admin_calendar_new():
# @app.route('/admin/kalendarz/nowy', methods=['GET', 'POST']) # MOVED TO admin.admin_calendar_new
# @login_required
def _old_admin_calendar_new():
"""Dodaj nowe wydarzenie"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
@ -2199,9 +2204,9 @@ def admin_calendar_new():
return render_template('calendar/admin_new.html')
@app.route('/admin/kalendarz/<int:event_id>/delete', methods=['POST'])
@login_required
def admin_calendar_delete(event_id):
# @app.route('/admin/kalendarz/<int:event_id>/delete', methods=['POST']) # MOVED TO admin.admin_calendar_delete
# @login_required
def _old_admin_calendar_delete(event_id):
"""Usuń wydarzenie"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403

View File

@ -195,7 +195,45 @@ def register_blueprints(app):
except Exception as e:
logger.error(f"Error registering chat blueprint: {e}")
# Phase 6-10: Future blueprints will be added here
# Phase 6: Admin blueprint (part 1: users, recommendations, fees, calendar)
try:
from blueprints.admin import bp as admin_bp
app.register_blueprint(admin_bp)
logger.info("Registered blueprint: admin")
# Create aliases for backward compatibility
_create_endpoint_aliases(app, admin_bp, {
# Recommendations
'admin_recommendations': 'admin.admin_recommendations',
'admin_recommendation_approve': 'admin.admin_recommendation_approve',
'admin_recommendation_reject': 'admin.admin_recommendation_reject',
# Users
'admin_users': 'admin.admin_users',
'admin_user_add': 'admin.admin_user_add',
'admin_user_toggle_admin': 'admin.admin_user_toggle_admin',
'admin_user_toggle_verified': 'admin.admin_user_toggle_verified',
'admin_user_update': 'admin.admin_user_update',
'admin_user_assign_company': 'admin.admin_user_assign_company',
'admin_user_delete': 'admin.admin_user_delete',
'admin_user_reset_password': 'admin.admin_user_reset_password',
# Fees
'admin_fees': 'admin.admin_fees',
'admin_fees_generate': 'admin.admin_fees_generate',
'admin_fees_mark_paid': 'admin.admin_fees_mark_paid',
'admin_fees_bulk_mark_paid': 'admin.admin_fees_bulk_mark_paid',
'admin_fees_export': 'admin.admin_fees_export',
# Calendar
'admin_calendar': 'admin.admin_calendar',
'admin_calendar_new': 'admin.admin_calendar_new',
'admin_calendar_delete': 'admin.admin_calendar_delete',
})
logger.info("Created admin endpoint aliases")
except ImportError as e:
logger.debug(f"Blueprint admin not yet available: {e}")
except Exception as e:
logger.error(f"Error registering admin blueprint: {e}")
# Phase 6 (continued) + Phase 7-10: Future blueprints will be added here
def _create_endpoint_aliases(app, blueprint, aliases):

View File

@ -0,0 +1,12 @@
"""
Admin Blueprint
===============
Admin panel routes: users, recommendations, fees, calendar, and more.
"""
from flask import Blueprint
bp = Blueprint('admin', __name__, url_prefix='/admin')
from . import routes # noqa: E402, F401

828
blueprints/admin/routes.py Normal file
View File

@ -0,0 +1,828 @@
"""
Admin Routes
============
Admin panel: users, recommendations, fees, calendar management.
"""
import os
import csv
import json
import re
import logging
import secrets
import string
from io import StringIO
from datetime import datetime, timedelta
from decimal import Decimal
from flask import render_template, request, redirect, url_for, flash, jsonify, Response
from flask_login import login_required, current_user
from werkzeug.security import generate_password_hash
from . import bp
from database import (
SessionLocal, User, Company, CompanyRecommendation,
MembershipFee, MembershipFeeConfig, NordaEvent, EventAttendee
)
import gemini_service
# Logger
logger = logging.getLogger(__name__)
# Polish month names for fees
MONTHS_PL = [
(1, 'Styczen'), (2, 'Luty'), (3, 'Marzec'), (4, 'Kwiecien'),
(5, 'Maj'), (6, 'Czerwiec'), (7, 'Lipiec'), (8, 'Sierpien'),
(9, 'Wrzesien'), (10, 'Pazdziernik'), (11, 'Listopad'), (12, 'Grudzien')
]
# ============================================================
# RECOMMENDATIONS ADMIN ROUTES
# ============================================================
@bp.route('/recommendations')
@login_required
def admin_recommendations():
"""Admin panel for recommendations moderation"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
recommendations = db.query(CompanyRecommendation).order_by(
CompanyRecommendation.created_at.desc()
).all()
pending_recommendations = db.query(CompanyRecommendation).filter(
CompanyRecommendation.status == 'pending'
).order_by(CompanyRecommendation.created_at.desc()).all()
total_recommendations = len(recommendations)
pending_count = len(pending_recommendations)
approved_count = db.query(CompanyRecommendation).filter(
CompanyRecommendation.status == 'approved'
).count()
rejected_count = db.query(CompanyRecommendation).filter(
CompanyRecommendation.status == 'rejected'
).count()
logger.info(f"Admin {current_user.email} accessed recommendations panel - {pending_count} pending")
return render_template(
'admin/recommendations.html',
recommendations=recommendations,
pending_recommendations=pending_recommendations,
total_recommendations=total_recommendations,
pending_count=pending_count,
approved_count=approved_count,
rejected_count=rejected_count
)
finally:
db.close()
@bp.route('/recommendations/<int:recommendation_id>/approve', methods=['POST'])
@login_required
def admin_recommendation_approve(recommendation_id):
"""Approve a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
recommendation = db.query(CompanyRecommendation).filter(
CompanyRecommendation.id == recommendation_id
).first()
if not recommendation:
return jsonify({'success': False, 'error': 'Rekomendacja nie istnieje'}), 404
recommendation.status = 'approved'
recommendation.moderated_by = current_user.id
recommendation.moderated_at = datetime.utcnow()
recommendation.rejection_reason = None
db.commit()
logger.info(f"Admin {current_user.email} approved recommendation #{recommendation_id}")
return jsonify({
'success': True,
'message': 'Rekomendacja zatwierdzona'
})
finally:
db.close()
@bp.route('/recommendations/<int:recommendation_id>/reject', methods=['POST'])
@login_required
def admin_recommendation_reject(recommendation_id):
"""Reject a recommendation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
recommendation = db.query(CompanyRecommendation).filter(
CompanyRecommendation.id == recommendation_id
).first()
if not recommendation:
return jsonify({'success': False, 'error': 'Rekomendacja nie istnieje'}), 404
rejection_reason = request.json.get('reason', '') if request.is_json else request.form.get('reason', '')
recommendation.status = 'rejected'
recommendation.moderated_by = current_user.id
recommendation.moderated_at = datetime.utcnow()
recommendation.rejection_reason = rejection_reason.strip() if rejection_reason else None
db.commit()
logger.info(f"Admin {current_user.email} rejected recommendation #{recommendation_id}")
return jsonify({
'success': True,
'message': 'Rekomendacja odrzucona'
})
finally:
db.close()
# ============================================================
# USER MANAGEMENT ADMIN ROUTES
# ============================================================
@bp.route('/users')
@login_required
def admin_users():
"""Admin panel for user management"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
users = db.query(User).order_by(User.created_at.desc()).all()
companies = db.query(Company).order_by(Company.name).all()
total_users = len(users)
admin_count = sum(1 for u in users if u.is_admin)
verified_count = sum(1 for u in users if u.is_verified)
unverified_count = total_users - verified_count
logger.info(f"Admin {current_user.email} accessed users panel - {total_users} users")
return render_template(
'admin/users.html',
users=users,
companies=companies,
total_users=total_users,
admin_count=admin_count,
verified_count=verified_count,
unverified_count=unverified_count
)
finally:
db.close()
@bp.route('/users/add', methods=['POST'])
@login_required
def admin_user_add():
"""Create a new user (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
email = data.get('email', '').strip().lower()
if not email:
return jsonify({'success': False, 'error': 'Email jest wymagany'}), 400
existing_user = db.query(User).filter(User.email == email).first()
if existing_user:
return jsonify({'success': False, 'error': 'Użytkownik z tym adresem email już istnieje'}), 400
password_chars = string.ascii_letters + string.digits + "!@#$%^&*"
generated_password = ''.join(secrets.choice(password_chars) for _ in range(16))
password_hash = generate_password_hash(generated_password, method='pbkdf2:sha256')
new_user = User(
email=email,
password_hash=password_hash,
name=data.get('name', '').strip() or None,
company_id=data.get('company_id') or None,
is_admin=data.get('is_admin', False),
is_verified=data.get('is_verified', True),
is_active=True
)
db.add(new_user)
db.commit()
db.refresh(new_user)
logger.info(f"Admin {current_user.email} created new user: {email} (ID: {new_user.id})")
return jsonify({
'success': True,
'user_id': new_user.id,
'generated_password': generated_password,
'message': f'Użytkownik {email} został utworzony'
})
except Exception as e:
db.rollback()
logger.error(f"Error creating user: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas tworzenia użytkownika'}), 500
finally:
db.close()
@bp.route('/users/<int:user_id>/toggle-admin', methods=['POST'])
@login_required
def admin_user_toggle_admin(user_id):
"""Toggle admin status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz zmienić własnych uprawnień'}), 400
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
user.is_admin = not user.is_admin
db.commit()
logger.info(f"Admin {current_user.email} {'granted' if user.is_admin else 'revoked'} admin for user {user.email}")
return jsonify({
'success': True,
'is_admin': user.is_admin,
'message': f"{'Nadano' if user.is_admin else 'Odebrano'} uprawnienia admina"
})
finally:
db.close()
@bp.route('/users/<int:user_id>/toggle-verified', methods=['POST'])
@login_required
def admin_user_toggle_verified(user_id):
"""Toggle verified status for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
user.is_verified = not user.is_verified
if user.is_verified:
user.verified_at = datetime.utcnow()
else:
user.verified_at = None
db.commit()
logger.info(f"Admin {current_user.email} {'verified' if user.is_verified else 'unverified'} user {user.email}")
return jsonify({
'success': True,
'is_verified': user.is_verified,
'message': f"Użytkownik {'zweryfikowany' if user.is_verified else 'niezweryfikowany'}"
})
finally:
db.close()
@bp.route('/users/<int:user_id>/update', methods=['POST'])
@login_required
def admin_user_update(user_id):
"""Update user data (name, email)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
data = request.get_json() or {}
if 'name' in data:
user.name = data['name'].strip() if data['name'] else None
if 'email' in data:
new_email = data['email'].strip().lower()
if new_email and new_email != user.email:
existing = db.query(User).filter(User.email == new_email, User.id != user_id).first()
if existing:
return jsonify({'success': False, 'error': 'Ten email jest już używany'}), 400
user.email = new_email
if 'phone' in data:
user.phone = data['phone'].strip() if data['phone'] else None
db.commit()
logger.info(f"Admin {current_user.email} updated user {user.email}: name={user.name}, phone={user.phone}")
return jsonify({
'success': True,
'user': {
'id': user.id,
'name': user.name,
'email': user.email,
'phone': user.phone
},
'message': 'Dane użytkownika zaktualizowane'
})
except Exception as e:
db.rollback()
logger.error(f"Error updating user {user_id}: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/users/<int:user_id>/assign-company', methods=['POST'])
@login_required
def admin_user_assign_company(user_id):
"""Assign a company to a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
data = request.get_json() or {}
company_id = data.get('company_id')
if company_id:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie znaleziona'}), 404
user.company_id = company_id
company_name = company.name
else:
user.company_id = None
company_name = None
db.commit()
logger.info(f"Admin {current_user.email} assigned company '{company_name}' to user {user.email}")
return jsonify({
'success': True,
'company_name': company_name,
'message': f"Przypisano firmę: {company_name}" if company_name else "Odłączono od firmy"
})
finally:
db.close()
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
@login_required
def admin_user_delete(user_id):
"""Delete a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz usunąć własnego konta'}), 400
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
email = user.email
db.delete(user)
db.commit()
logger.info(f"Admin {current_user.email} deleted user {email}")
return jsonify({
'success': True,
'message': f"Użytkownik {email} został usunięty"
})
finally:
db.close()
@bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
def admin_user_reset_password(user_id):
"""Generate password reset token for a user"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
reset_token = secrets.token_urlsafe(32)
user.reset_token = reset_token
user.reset_token_expires = datetime.utcnow() + timedelta(hours=1)
db.commit()
base_url = os.getenv('APP_URL', 'https://nordabiznes.pl')
reset_url = f"{base_url}/reset-password/{reset_token}"
logger.info(f"Admin {current_user.email} generated reset token for user {user.email}: {reset_token[:8]}...")
return jsonify({
'success': True,
'reset_url': reset_url,
'message': f"Link do resetu hasła wygenerowany (ważny 1 godzinę)"
})
finally:
db.close()
# ============================================================
# MEMBERSHIP FEES ADMIN
# ============================================================
@bp.route('/fees')
@login_required
def admin_fees():
"""Admin panel for membership fee management"""
if not current_user.is_admin:
flash('Brak uprawnien do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
from sqlalchemy import func, case
year = request.args.get('year', datetime.now().year, type=int)
month = request.args.get('month', type=int)
status_filter = request.args.get('status', '')
companies = db.query(Company).filter(Company.status == 'active').order_by(Company.name).all()
fee_query = db.query(MembershipFee).filter(MembershipFee.fee_year == year)
if month:
fee_query = fee_query.filter(MembershipFee.fee_month == month)
fees = {(f.company_id, f.fee_month): f for f in fee_query.all()}
companies_fees = []
for company in companies:
if month:
fee = fees.get((company.id, month))
companies_fees.append({
'company': company,
'fee': fee,
'status': fee.status if fee else 'brak'
})
else:
company_data = {'company': company, 'months': {}}
for m in range(1, 13):
fee = fees.get((company.id, m))
company_data['months'][m] = fee
companies_fees.append(company_data)
if status_filter and month:
if status_filter == 'paid':
companies_fees = [cf for cf in companies_fees if cf.get('status') == 'paid']
elif status_filter == 'pending':
companies_fees = [cf for cf in companies_fees if cf.get('status') in ('pending', 'brak')]
elif status_filter == 'overdue':
companies_fees = [cf for cf in companies_fees if cf.get('status') == 'overdue']
total_companies = len(companies)
if month:
month_fees = [cf.get('fee') for cf in companies_fees if cf.get('fee')]
paid_count = sum(1 for f in month_fees if f and f.status == 'paid')
pending_count = total_companies - paid_count
total_due = sum(float(f.amount) for f in month_fees if f) if month_fees else Decimal(0)
total_paid = sum(float(f.amount_paid or 0) for f in month_fees if f) if month_fees else Decimal(0)
else:
all_fees = list(fees.values())
paid_count = sum(1 for f in all_fees if f.status == 'paid')
pending_count = len(all_fees) - paid_count
total_due = sum(float(f.amount) for f in all_fees) if all_fees else Decimal(0)
total_paid = sum(float(f.amount_paid or 0) for f in all_fees) if all_fees else Decimal(0)
fee_config = db.query(MembershipFeeConfig).filter(
MembershipFeeConfig.scope == 'global',
MembershipFeeConfig.valid_until == None
).first()
default_fee = float(fee_config.monthly_amount) if fee_config else 100.00
return render_template(
'admin/fees.html',
companies_fees=companies_fees,
year=year,
month=month,
status_filter=status_filter,
total_companies=total_companies,
paid_count=paid_count,
pending_count=pending_count,
total_due=total_due,
total_paid=total_paid,
default_fee=default_fee,
years=list(range(2024, datetime.now().year + 2)),
months=MONTHS_PL
)
finally:
db.close()
@bp.route('/fees/generate', methods=['POST'])
@login_required
def admin_fees_generate():
"""Generate fee records for all companies for a given month"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal()
try:
year = request.form.get('year', type=int)
month = request.form.get('month', type=int)
if not year or not month:
return jsonify({'success': False, 'error': 'Brak roku lub miesiaca'}), 400
fee_config = db.query(MembershipFeeConfig).filter(
MembershipFeeConfig.scope == 'global',
MembershipFeeConfig.valid_until == None
).first()
default_fee = fee_config.monthly_amount if fee_config else 100.00
companies = db.query(Company).filter(Company.status == 'active').all()
created = 0
for company in companies:
existing = db.query(MembershipFee).filter(
MembershipFee.company_id == company.id,
MembershipFee.fee_year == year,
MembershipFee.fee_month == month
).first()
if not existing:
fee = MembershipFee(
company_id=company.id,
fee_year=year,
fee_month=month,
amount=default_fee,
status='pending'
)
db.add(fee)
created += 1
db.commit()
return jsonify({
'success': True,
'message': f'Utworzono {created} rekordow skladek'
})
except Exception as e:
db.rollback()
logger.error(f"Error generating fees: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/fees/<int:fee_id>/mark-paid', methods=['POST'])
@login_required
def admin_fees_mark_paid(fee_id):
"""Mark a fee as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal()
try:
fee = db.query(MembershipFee).filter(MembershipFee.id == fee_id).first()
if not fee:
return jsonify({'success': False, 'error': 'Nie znaleziono skladki'}), 404
amount_paid = request.form.get('amount_paid', type=float)
payment_date = request.form.get('payment_date')
payment_method = request.form.get('payment_method', 'transfer')
payment_reference = request.form.get('payment_reference', '')
notes = request.form.get('notes', '')
fee.amount_paid = amount_paid or float(fee.amount)
fee.payment_date = datetime.strptime(payment_date, '%Y-%m-%d').date() if payment_date else datetime.now().date()
fee.payment_method = payment_method
fee.payment_reference = payment_reference
fee.notes = notes
fee.recorded_by = current_user.id
fee.recorded_at = datetime.now()
if fee.amount_paid >= float(fee.amount):
fee.status = 'paid'
elif fee.amount_paid > 0:
fee.status = 'partial'
db.commit()
return jsonify({
'success': True,
'message': 'Skladka zostala zarejestrowana'
})
except Exception as e:
db.rollback()
logger.error(f"Error marking fee as paid: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/fees/bulk-mark-paid', methods=['POST'])
@login_required
def admin_fees_bulk_mark_paid():
"""Bulk mark fees as paid"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
db = SessionLocal()
try:
fee_ids = request.form.getlist('fee_ids[]', type=int)
if not fee_ids:
return jsonify({'success': False, 'error': 'Brak wybranych skladek'}), 400
updated = 0
for fee_id in fee_ids:
fee = db.query(MembershipFee).filter(MembershipFee.id == fee_id).first()
if fee and fee.status != 'paid':
fee.status = 'paid'
fee.amount_paid = fee.amount
fee.payment_date = datetime.now().date()
fee.recorded_by = current_user.id
fee.recorded_at = datetime.now()
updated += 1
db.commit()
return jsonify({
'success': True,
'message': f'Zaktualizowano {updated} rekordow'
})
except Exception as e:
db.rollback()
logger.error(f"Error in bulk action: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/fees/export')
@login_required
def admin_fees_export():
"""Export fees to CSV"""
if not current_user.is_admin:
flash('Brak uprawnien.', 'error')
return redirect(url_for('.admin_fees'))
db = SessionLocal()
try:
year = request.args.get('year', datetime.now().year, type=int)
month = request.args.get('month', type=int)
query = db.query(MembershipFee).join(Company).filter(
MembershipFee.fee_year == year
)
if month:
query = query.filter(MembershipFee.fee_month == month)
fees = query.order_by(Company.name, MembershipFee.fee_month).all()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
'Firma', 'NIP', 'Rok', 'Miesiac', 'Kwota', 'Zaplacono',
'Status', 'Data platnosci', 'Metoda', 'Referencja', 'Notatki'
])
for fee in fees:
writer.writerow([
fee.company.name,
fee.company.nip,
fee.fee_year,
fee.fee_month,
fee.amount,
fee.amount_paid,
fee.status,
fee.payment_date,
fee.payment_method,
fee.payment_reference,
fee.notes
])
output.seek(0)
return Response(
output.getvalue(),
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename=skladki_{year}_{month or "all"}.csv'
}
)
finally:
db.close()
# ============================================================
# CALENDAR ADMIN ROUTES
# ============================================================
@bp.route('/kalendarz')
@login_required
def admin_calendar():
"""Panel admin - zarządzanie wydarzeniami"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
db = SessionLocal()
try:
events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).all()
return render_template('admin/calendar.html', events=events)
finally:
db.close()
@bp.route('/kalendarz/nowy', methods=['GET', 'POST'])
@login_required
def admin_calendar_new():
"""Dodaj nowe wydarzenie"""
if not current_user.is_admin:
flash('Brak uprawnień.', 'error')
return redirect(url_for('calendar_index'))
if request.method == 'POST':
db = SessionLocal()
try:
event = NordaEvent(
title=request.form.get('title', '').strip(),
description=request.form.get('description', '').strip(),
event_date=datetime.strptime(request.form.get('event_date'), '%Y-%m-%d').date(),
start_time=request.form.get('start_time') or None,
end_time=request.form.get('end_time') or None,
location=request.form.get('location', '').strip() or None,
event_type=request.form.get('event_type', 'meeting'),
max_participants=request.form.get('max_participants', type=int) or None,
registration_required=request.form.get('registration_required') == 'on',
is_public=request.form.get('is_public') == 'on',
created_by=current_user.id
)
db.add(event)
db.commit()
flash('Wydarzenie zostało dodane.', 'success')
return redirect(url_for('.admin_calendar'))
except Exception as e:
db.rollback()
flash(f'Błąd: {str(e)}', 'error')
finally:
db.close()
return render_template('admin/calendar_form.html', event=None)
@bp.route('/kalendarz/<int:event_id>/delete', methods=['POST'])
@login_required
def admin_calendar_delete(event_id):
"""Usuń wydarzenie"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
event = db.query(NordaEvent).filter(NordaEvent.id == event_id).first()
if not event:
return jsonify({'success': False, 'error': 'Wydarzenie nie istnieje'}), 404
# Delete RSVPs first
db.query(EventAttendee).filter(EventAttendee.event_id == event_id).delete()
db.delete(event)
db.commit()
return jsonify({'success': True, 'message': 'Wydarzenie usunięte'})
except Exception as e:
db.rollback()
logger.error(f"Error deleting event: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()