Implement full online membership application workflow: - 3-step wizard form with KRS/CEIDG auto-fill - Admin panel for application review (approve/reject/request changes) - Company data update requests for existing members - Dashboard CTA for users without company - API endpoints for NIP lookup and draft management New files: - database/migrations/042_membership_applications.sql - blueprints/membership/ (routes, templates) - blueprints/admin/routes_membership.py - blueprints/api/routes_membership.py - templates/membership/ and templates/admin/membership*.html Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
550 lines
18 KiB
Python
550 lines
18 KiB
Python
"""
|
|
Admin Routes - Membership Applications
|
|
=======================================
|
|
|
|
Admin panel for managing membership applications and company data requests.
|
|
"""
|
|
|
|
import re
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from flask import render_template, request, redirect, url_for, flash, jsonify
|
|
from flask_login import login_required, current_user
|
|
|
|
from . import bp
|
|
from database import (
|
|
SessionLocal, MembershipApplication, CompanyDataRequest,
|
|
Company, Category, User
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _generate_slug(name):
|
|
"""Generate URL-safe slug from company name."""
|
|
slug = re.sub(r'[^\w\s-]', '', name.lower())
|
|
slug = re.sub(r'[\s_]+', '-', slug)
|
|
slug = re.sub(r'-+', '-', slug).strip('-')
|
|
return slug
|
|
|
|
|
|
def _generate_member_number(db):
|
|
"""Generate next member number."""
|
|
last = db.query(MembershipApplication).filter(
|
|
MembershipApplication.member_number.isnot(None)
|
|
).order_by(MembershipApplication.id.desc()).first()
|
|
|
|
if last and last.member_number:
|
|
try:
|
|
num = int(last.member_number.replace('NB-', ''))
|
|
return f"NB-{num + 1:04d}"
|
|
except ValueError:
|
|
pass
|
|
|
|
# Count existing companies as starting point
|
|
count = db.query(Company).filter(Company.status == 'active').count()
|
|
return f"NB-{count + 1:04d}"
|
|
|
|
|
|
# ============================================================
|
|
# MEMBERSHIP APPLICATIONS
|
|
# ============================================================
|
|
|
|
@bp.route('/membership')
|
|
@login_required
|
|
def admin_membership():
|
|
"""Admin panel for membership applications."""
|
|
if not current_user.is_admin:
|
|
flash('Brak uprawnień do tej strony.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Get filter parameters
|
|
status_filter = request.args.get('status', 'all')
|
|
search_query = request.args.get('q', '').strip()
|
|
|
|
# Base query
|
|
query = db.query(MembershipApplication)
|
|
|
|
# Apply filters
|
|
if status_filter and status_filter != 'all':
|
|
query = query.filter(MembershipApplication.status == status_filter)
|
|
|
|
if search_query:
|
|
search_pattern = f'%{search_query}%'
|
|
query = query.filter(
|
|
(MembershipApplication.company_name.ilike(search_pattern)) |
|
|
(MembershipApplication.nip.ilike(search_pattern)) |
|
|
(MembershipApplication.email.ilike(search_pattern))
|
|
)
|
|
|
|
# Order by submitted date (newest first)
|
|
applications = query.order_by(
|
|
MembershipApplication.submitted_at.desc().nullslast(),
|
|
MembershipApplication.created_at.desc()
|
|
).all()
|
|
|
|
# Statistics
|
|
total = db.query(MembershipApplication).count()
|
|
submitted = db.query(MembershipApplication).filter(
|
|
MembershipApplication.status == 'submitted'
|
|
).count()
|
|
under_review = db.query(MembershipApplication).filter(
|
|
MembershipApplication.status == 'under_review'
|
|
).count()
|
|
approved = db.query(MembershipApplication).filter(
|
|
MembershipApplication.status == 'approved'
|
|
).count()
|
|
rejected = db.query(MembershipApplication).filter(
|
|
MembershipApplication.status == 'rejected'
|
|
).count()
|
|
|
|
logger.info(f"Admin {current_user.email} accessed membership applications panel")
|
|
|
|
return render_template(
|
|
'admin/membership.html',
|
|
applications=applications,
|
|
total=total,
|
|
submitted=submitted,
|
|
under_review=under_review,
|
|
approved=approved,
|
|
rejected=rejected,
|
|
current_status=status_filter,
|
|
search_query=search_query,
|
|
status_choices=MembershipApplication.STATUS_CHOICES
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/<int:app_id>')
|
|
@login_required
|
|
def admin_membership_detail(app_id):
|
|
"""View membership application details."""
|
|
if not current_user.is_admin:
|
|
flash('Brak uprawnień do tej strony.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).get(app_id)
|
|
if not application:
|
|
flash('Nie znaleziono deklaracji.', 'error')
|
|
return redirect(url_for('admin.admin_membership'))
|
|
|
|
# Get categories for company creation
|
|
categories = db.query(Category).order_by(Category.name).all()
|
|
|
|
return render_template(
|
|
'admin/membership_detail.html',
|
|
application=application,
|
|
categories=categories,
|
|
section_choices=MembershipApplication.SECTION_CHOICES,
|
|
business_type_choices=MembershipApplication.BUSINESS_TYPE_CHOICES
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/<int:app_id>/approve', methods=['POST'])
|
|
@login_required
|
|
def admin_membership_approve(app_id):
|
|
"""Approve membership application and create company."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).get(app_id)
|
|
if not application:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
|
|
|
if application.status not in ['submitted', 'under_review']:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Nie można zatwierdzić deklaracji w statusie: {application.status}'
|
|
}), 400
|
|
|
|
data = request.get_json() or {}
|
|
|
|
# Check if company with this NIP already exists
|
|
existing = db.query(Company).filter(Company.nip == application.nip).first()
|
|
if existing:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Firma z NIP {application.nip} już istnieje w katalogu'
|
|
}), 400
|
|
|
|
# Generate unique slug
|
|
slug = _generate_slug(application.company_name)
|
|
base_slug = slug
|
|
counter = 1
|
|
while db.query(Company).filter(Company.slug == slug).first():
|
|
slug = f"{base_slug}-{counter}"
|
|
counter += 1
|
|
|
|
# Create company
|
|
company = Company(
|
|
name=application.company_name,
|
|
slug=slug,
|
|
nip=application.nip,
|
|
regon=application.regon,
|
|
krs=application.krs_number,
|
|
website=application.website,
|
|
email=application.email,
|
|
phone=application.phone,
|
|
address_postal_code=application.address_postal_code,
|
|
address_city=application.address_city,
|
|
address_street=application.address_street,
|
|
address_number=application.address_number,
|
|
description_short=application.description[:500] if application.description else None,
|
|
description_full=application.description,
|
|
founded_date=application.founded_date,
|
|
employee_count=application.employee_count,
|
|
show_employee_count=application.show_employee_count,
|
|
category_id=data.get('category_id'),
|
|
status='active',
|
|
is_norda_member=True,
|
|
data_quality='enhanced'
|
|
)
|
|
|
|
db.add(company)
|
|
db.flush() # Get company ID
|
|
|
|
# Update application
|
|
application.status = 'approved'
|
|
application.reviewed_at = datetime.now()
|
|
application.reviewed_by_id = current_user.id
|
|
application.review_comment = data.get('comment', '')
|
|
application.company_id = company.id
|
|
application.member_number = _generate_member_number(db)
|
|
|
|
# Assign user to company
|
|
user = db.query(User).get(application.user_id)
|
|
if user:
|
|
user.company_id = company.id
|
|
user.is_norda_member = True
|
|
|
|
db.commit()
|
|
|
|
logger.info(
|
|
f"Membership application {app_id} approved by {current_user.email}. "
|
|
f"Company {company.id} created. Member number: {application.member_number}"
|
|
)
|
|
|
|
flash(f'Deklaracja zatwierdzona. Utworzono firmę: {company.name}', 'success')
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'company_id': company.id,
|
|
'member_number': application.member_number
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error approving application {app_id}: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/<int:app_id>/reject', methods=['POST'])
|
|
@login_required
|
|
def admin_membership_reject(app_id):
|
|
"""Reject membership application."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).get(app_id)
|
|
if not application:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
|
|
|
if application.status not in ['submitted', 'under_review']:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Nie można odrzucić deklaracji w statusie: {application.status}'
|
|
}), 400
|
|
|
|
data = request.get_json() or {}
|
|
comment = data.get('comment', '').strip()
|
|
|
|
if not comment:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Podaj powód odrzucenia'
|
|
}), 400
|
|
|
|
application.status = 'rejected'
|
|
application.reviewed_at = datetime.now()
|
|
application.reviewed_by_id = current_user.id
|
|
application.review_comment = comment
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Membership application {app_id} rejected by {current_user.email}: {comment}")
|
|
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error rejecting application {app_id}: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/<int:app_id>/request-changes', methods=['POST'])
|
|
@login_required
|
|
def admin_membership_request_changes(app_id):
|
|
"""Request changes to membership application."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).get(app_id)
|
|
if not application:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
|
|
|
if application.status not in ['submitted', 'under_review']:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Nie można poprosić o poprawki dla deklaracji w statusie: {application.status}'
|
|
}), 400
|
|
|
|
data = request.get_json() or {}
|
|
comment = data.get('comment', '').strip()
|
|
|
|
if not comment:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Podaj co wymaga poprawienia'
|
|
}), 400
|
|
|
|
application.status = 'changes_requested'
|
|
application.reviewed_at = datetime.now()
|
|
application.reviewed_by_id = current_user.id
|
|
application.review_comment = comment
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Changes requested for application {app_id} by {current_user.email}: {comment}")
|
|
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error requesting changes for application {app_id}: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/<int:app_id>/start-review', methods=['POST'])
|
|
@login_required
|
|
def admin_membership_start_review(app_id):
|
|
"""Mark application as under review."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).get(app_id)
|
|
if not application:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
|
|
|
if application.status != 'submitted':
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Można rozpocząć rozpatrywanie tylko dla wysłanych deklaracji'
|
|
}), 400
|
|
|
|
application.status = 'under_review'
|
|
application.reviewed_by_id = current_user.id
|
|
db.commit()
|
|
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
db.rollback()
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# COMPANY DATA REQUESTS
|
|
# ============================================================
|
|
|
|
@bp.route('/company-requests')
|
|
@login_required
|
|
def admin_company_requests():
|
|
"""Admin panel for company data requests."""
|
|
if not current_user.is_admin:
|
|
flash('Brak uprawnień do tej strony.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
status_filter = request.args.get('status', 'pending')
|
|
|
|
query = db.query(CompanyDataRequest)
|
|
|
|
if status_filter and status_filter != 'all':
|
|
query = query.filter(CompanyDataRequest.status == status_filter)
|
|
|
|
requests_list = query.order_by(CompanyDataRequest.created_at.desc()).all()
|
|
|
|
# Statistics
|
|
pending = db.query(CompanyDataRequest).filter(
|
|
CompanyDataRequest.status == 'pending'
|
|
).count()
|
|
approved = db.query(CompanyDataRequest).filter(
|
|
CompanyDataRequest.status == 'approved'
|
|
).count()
|
|
rejected = db.query(CompanyDataRequest).filter(
|
|
CompanyDataRequest.status == 'rejected'
|
|
).count()
|
|
|
|
return render_template(
|
|
'admin/company_requests.html',
|
|
requests=requests_list,
|
|
pending=pending,
|
|
approved=approved,
|
|
rejected=rejected,
|
|
current_status=status_filter
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company-requests/<int:req_id>/approve', methods=['POST'])
|
|
@login_required
|
|
def admin_company_request_approve(req_id):
|
|
"""Approve company data request and update company."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
data_request = db.query(CompanyDataRequest).get(req_id)
|
|
if not data_request:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono zgłoszenia'}), 404
|
|
|
|
if data_request.status != 'pending':
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Zgłoszenie już rozpatrzone: {data_request.status}'
|
|
}), 400
|
|
|
|
company = db.query(Company).get(data_request.company_id)
|
|
if not company:
|
|
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
|
|
|
|
fetched = data_request.fetched_data or {}
|
|
applied_fields = []
|
|
|
|
# Update company with fetched data
|
|
if fetched.get('name') and not company.name:
|
|
company.name = fetched['name']
|
|
applied_fields.append('name')
|
|
|
|
if data_request.nip and not company.nip:
|
|
company.nip = data_request.nip
|
|
applied_fields.append('nip')
|
|
|
|
if fetched.get('regon') and not company.regon:
|
|
company.regon = fetched['regon']
|
|
applied_fields.append('regon')
|
|
|
|
if fetched.get('krs') and not company.krs:
|
|
company.krs = fetched['krs']
|
|
applied_fields.append('krs')
|
|
|
|
if fetched.get('address_postal_code') and not company.address_postal_code:
|
|
company.address_postal_code = fetched['address_postal_code']
|
|
applied_fields.append('address_postal_code')
|
|
|
|
if fetched.get('address_city') and not company.address_city:
|
|
company.address_city = fetched['address_city']
|
|
applied_fields.append('address_city')
|
|
|
|
if fetched.get('address_street') and not company.address_street:
|
|
company.address_street = fetched['address_street']
|
|
applied_fields.append('address_street')
|
|
|
|
if fetched.get('address_number') and not company.address_number:
|
|
company.address_number = fetched['address_number']
|
|
applied_fields.append('address_number')
|
|
|
|
if fetched.get('website') and not company.website:
|
|
company.website = fetched['website']
|
|
applied_fields.append('website')
|
|
|
|
if fetched.get('email') and not company.email:
|
|
company.email = fetched['email']
|
|
applied_fields.append('email')
|
|
|
|
# Update request
|
|
data_request.status = 'approved'
|
|
data_request.reviewed_at = datetime.now()
|
|
data_request.reviewed_by_id = current_user.id
|
|
data_request.applied_fields = applied_fields
|
|
|
|
db.commit()
|
|
|
|
logger.info(
|
|
f"Company data request {req_id} approved by {current_user.email}. "
|
|
f"Updated fields: {applied_fields}"
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'applied_fields': applied_fields
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error approving data request {req_id}: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company-requests/<int:req_id>/reject', methods=['POST'])
|
|
@login_required
|
|
def admin_company_request_reject(req_id):
|
|
"""Reject company data request."""
|
|
if not current_user.is_admin:
|
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
data_request = db.query(CompanyDataRequest).get(req_id)
|
|
if not data_request:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono zgłoszenia'}), 404
|
|
|
|
if data_request.status != 'pending':
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Zgłoszenie już rozpatrzone: {data_request.status}'
|
|
}), 400
|
|
|
|
data = request.get_json() or {}
|
|
comment = data.get('comment', '').strip()
|
|
|
|
data_request.status = 'rejected'
|
|
data_request.reviewed_at = datetime.now()
|
|
data_request.reviewed_by_id = current_user.id
|
|
data_request.review_comment = comment
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Company data request {req_id} rejected by {current_user.email}")
|
|
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
db.rollback()
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|