nordabiz/blueprints/admin/routes_companies.py
Maciej Pienczyn d5273a8e6a feat: Add admin modules for Companies and People management
- Add /admin/companies with CRUD operations, filters, CSV export
- Add /admin/people with person-company relationship management
- Companies: add, edit, toggle status, archive, view linked people
- People: add, edit, delete, link/unlink companies by role
- Both panels follow existing admin UI patterns (stats, filters, modals)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 20:13:51 +01:00

499 lines
16 KiB
Python

"""
Admin Routes - Companies
========================
CRUD operations for company management in admin panel.
"""
import re
import csv
import logging
from io import StringIO
from datetime import datetime
from flask import render_template, request, redirect, url_for, flash, jsonify, Response
from flask_login import login_required, current_user
from . import bp
from database import SessionLocal, Company, Category, User, Person, CompanyPerson
# Logger
logger = logging.getLogger(__name__)
def validate_nip(nip: str) -> bool:
"""Validate Polish NIP number (10 digits, checksum)"""
if not nip or not re.match(r'^\d{10}$', nip):
return False
weights = [6, 5, 7, 2, 3, 4, 5, 6, 7]
checksum = sum(int(nip[i]) * weights[i] for i in range(9)) % 11
return checksum == int(nip[9])
# ============================================================
# COMPANIES ADMIN ROUTES
# ============================================================
@bp.route('/companies')
@login_required
def admin_companies():
"""Admin panel for company management"""
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')
category_filter = request.args.get('category', '')
quality_filter = request.args.get('quality', '')
search_query = request.args.get('q', '').strip()
# Base query
query = db.query(Company)
# Apply filters
if status_filter and status_filter != 'all':
query = query.filter(Company.status == status_filter)
if category_filter:
query = query.filter(Company.category_id == int(category_filter))
if quality_filter:
query = query.filter(Company.data_quality == quality_filter)
if search_query:
search_pattern = f'%{search_query}%'
query = query.filter(
(Company.name.ilike(search_pattern)) |
(Company.nip.ilike(search_pattern))
)
# Order and fetch
companies = query.order_by(Company.name).all()
# Get categories for filter dropdown
categories = db.query(Category).order_by(Category.name).all()
# Statistics
total_companies = db.query(Company).count()
active_count = db.query(Company).filter(Company.status == 'active').count()
pending_count = db.query(Company).filter(Company.status == 'pending').count()
inactive_count = db.query(Company).filter(Company.status == 'inactive').count()
logger.info(f"Admin {current_user.email} accessed companies panel - {total_companies} companies")
return render_template(
'admin/companies.html',
companies=companies,
categories=categories,
total_companies=total_companies,
active_count=active_count,
pending_count=pending_count,
inactive_count=inactive_count,
current_status=status_filter,
current_category=category_filter,
current_quality=quality_filter,
search_query=search_query
)
finally:
db.close()
@bp.route('/companies/add', methods=['POST'])
@login_required
def admin_company_add():
"""Create a new company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'error': 'Nazwa firmy jest wymagana'}), 400
nip = data.get('nip', '').strip().replace('-', '').replace(' ', '')
if nip:
if not validate_nip(nip):
return jsonify({'success': False, 'error': 'Nieprawidłowy NIP'}), 400
existing = db.query(Company).filter(Company.nip == nip).first()
if existing:
return jsonify({'success': False, 'error': f'Firma z NIP {nip} już istnieje'}), 400
# Generate slug from name
slug = re.sub(r'[^\w\s-]', '', name.lower())
slug = re.sub(r'[\s_]+', '-', slug)
slug = re.sub(r'-+', '-', slug).strip('-')
# Ensure unique slug
base_slug = slug
counter = 1
while db.query(Company).filter(Company.slug == slug).first():
slug = f"{base_slug}-{counter}"
counter += 1
new_company = Company(
name=name,
slug=slug,
nip=nip if nip else None,
category_id=data.get('category_id') or None,
status=data.get('status', 'pending'),
email=data.get('email', '').strip() or None,
phone=data.get('phone', '').strip() or None,
address_city=data.get('address_city', '').strip() or None,
address_street=data.get('address_street', '').strip() or None,
address_postal=data.get('address_postal', '').strip() or None,
data_quality='basic'
)
db.add(new_company)
db.commit()
db.refresh(new_company)
logger.info(f"Admin {current_user.email} created new company: {name} (ID: {new_company.id})")
return jsonify({
'success': True,
'company_id': new_company.id,
'message': f'Firma "{name}" została utworzona'
})
except Exception as e:
db.rollback()
logger.error(f"Error creating company: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas tworzenia firmy'}), 500
finally:
db.close()
@bp.route('/companies/<int:company_id>')
@login_required
def admin_company_get(company_id):
"""Get company details (JSON)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
return jsonify({
'success': True,
'company': {
'id': company.id,
'name': company.name,
'nip': company.nip,
'category_id': company.category_id,
'status': company.status,
'email': company.email,
'phone': company.phone,
'address_city': company.address_city,
'address_street': company.address_street,
'address_postal': company.address_postal,
'data_quality': company.data_quality
}
})
finally:
db.close()
@bp.route('/companies/<int:company_id>/update', methods=['POST'])
@login_required
def admin_company_update(company_id):
"""Update company data"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
data = request.get_json() or {}
if 'name' in data:
name = data['name'].strip()
if not name:
return jsonify({'success': False, 'error': 'Nazwa firmy jest wymagana'}), 400
company.name = name
if 'nip' in data:
nip = data['nip'].strip().replace('-', '').replace(' ', '') if data['nip'] else ''
if nip:
if not validate_nip(nip):
return jsonify({'success': False, 'error': 'Nieprawidłowy NIP'}), 400
existing = db.query(Company).filter(Company.nip == nip, Company.id != company_id).first()
if existing:
return jsonify({'success': False, 'error': f'Firma z NIP {nip} już istnieje'}), 400
company.nip = nip if nip else None
if 'category_id' in data:
company.category_id = data['category_id'] if data['category_id'] else None
if 'status' in data:
if data['status'] in ['active', 'pending', 'inactive', 'archived']:
company.status = data['status']
if 'email' in data:
company.email = data['email'].strip() if data['email'] else None
if 'phone' in data:
company.phone = data['phone'].strip() if data['phone'] else None
if 'address_city' in data:
company.address_city = data['address_city'].strip() if data['address_city'] else None
if 'address_street' in data:
company.address_street = data['address_street'].strip() if data['address_street'] else None
if 'address_postal' in data:
company.address_postal = data['address_postal'].strip() if data['address_postal'] else None
company.last_updated = datetime.utcnow()
db.commit()
logger.info(f"Admin {current_user.email} updated company {company.name} (ID: {company_id})")
return jsonify({
'success': True,
'message': 'Dane firmy zaktualizowane'
})
except Exception as e:
db.rollback()
logger.error(f"Error updating company {company_id}: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/companies/<int:company_id>/toggle-status', methods=['POST'])
@login_required
def admin_company_toggle_status(company_id):
"""Toggle company status (active <-> inactive)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
if company.status == 'active':
company.status = 'inactive'
else:
company.status = 'active'
company.last_updated = datetime.utcnow()
db.commit()
logger.info(f"Admin {current_user.email} toggled company {company.name} status to {company.status}")
return jsonify({
'success': True,
'status': company.status,
'message': f"Status zmieniony na {'aktywna' if company.status == 'active' else 'nieaktywna'}"
})
finally:
db.close()
@bp.route('/companies/<int:company_id>/delete', methods=['POST'])
@login_required
def admin_company_delete(company_id):
"""Soft delete company (set status to archived)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
company.status = 'archived'
company.last_updated = datetime.utcnow()
db.commit()
logger.info(f"Admin {current_user.email} archived company {company.name} (ID: {company_id})")
return jsonify({
'success': True,
'message': f'Firma "{company.name}" została zarchiwizowana'
})
finally:
db.close()
@bp.route('/companies/<int:company_id>/assign-user', methods=['POST'])
@login_required
def admin_company_assign_user(company_id):
"""Assign a user to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
data = request.get_json() or {}
user_id = data.get('user_id')
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
if user_id:
user = db.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie istnieje'}), 404
user.company_id = company_id
db.commit()
logger.info(f"Admin {current_user.email} assigned user {user.email} to company {company.name}")
return jsonify({
'success': True,
'message': f'Użytkownik {user.email} przypisany do {company.name}'
})
else:
return jsonify({'success': False, 'error': 'Nie podano ID użytkownika'}), 400
finally:
db.close()
@bp.route('/companies/<int:company_id>/people')
@login_required
def admin_company_people(company_id):
"""Get people associated with a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
# Get CompanyPerson relationships
people_roles = db.query(CompanyPerson).filter(
CompanyPerson.company_id == company_id
).all()
people_list = []
for cp in people_roles:
person = cp.person
people_list.append({
'id': cp.id,
'person_id': person.id,
'imiona': person.imiona,
'nazwisko': person.nazwisko,
'pesel_masked': f"{person.pesel[:4]}******" if person.pesel else None,
'role': cp.role,
'role_category': cp.role_category,
'shares_percent': float(cp.shares_percent) if cp.shares_percent else None
})
return jsonify({
'success': True,
'company_name': company.name,
'people': people_list
})
finally:
db.close()
@bp.route('/companies/<int:company_id>/users')
@login_required
def admin_company_users(company_id):
"""Get users assigned to a company"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
users = db.query(User).filter(User.company_id == company_id).all()
users_list = [{
'id': u.id,
'name': u.name,
'email': u.email,
'is_admin': u.is_admin,
'is_verified': u.is_verified
} for u in users]
return jsonify({
'success': True,
'company_name': company.name,
'users': users_list
})
finally:
db.close()
@bp.route('/companies/export')
@login_required
def admin_companies_export():
"""Export companies to CSV"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('index'))
db = SessionLocal()
try:
companies = db.query(Company).order_by(Company.name).all()
output = StringIO()
writer = csv.writer(output)
# Header
writer.writerow([
'ID', 'Nazwa', 'NIP', 'Kategoria', 'Status',
'Email', 'Telefon', 'Miasto', 'Ulica', 'Kod pocztowy',
'Jakość danych', 'Data utworzenia'
])
# Data rows
for c in companies:
writer.writerow([
c.id,
c.name,
c.nip or '',
c.category.name if c.category else '',
c.status or '',
c.email or '',
c.phone or '',
c.address_city or '',
c.address_street or '',
c.address_postal or '',
c.data_quality or '',
c.created_at.strftime('%Y-%m-%d') if c.created_at else ''
])
output.seek(0)
logger.info(f"Admin {current_user.email} exported {len(companies)} companies to CSV")
return Response(
output.getvalue(),
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename=companies_{datetime.now().strftime("%Y%m%d")}.csv'
}
)
finally:
db.close()