From ff35ef6b43dab85be7ca4720877e31206edc2008 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Fri, 20 Feb 2026 13:29:00 +0100 Subject: [PATCH] feat: add admin company detail page with enrichment workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New admin page at /admin/companies/{id}/detail showing company data, completeness score, and action buttons for registry data (CEIDG/KRS), logo fetch, SEO audit, social media audit, and GBP audit. Includes "Uzbrój firmę" master button for sequential execution. Company list now links to detail page with "NOWA" badge for recent entries. Co-Authored-By: Claude Opus 4.6 --- blueprints/admin/routes_companies.py | 123 +++- templates/admin/companies.html | 20 +- templates/admin/company_detail.html | 815 +++++++++++++++++++++++++++ 3 files changed, 955 insertions(+), 3 deletions(-) create mode 100644 templates/admin/company_detail.html diff --git a/blueprints/admin/routes_companies.py b/blueprints/admin/routes_companies.py index 8158d0d..abc9800 100644 --- a/blueprints/admin/routes_companies.py +++ b/blueprints/admin/routes_companies.py @@ -5,6 +5,7 @@ Admin Routes - Companies CRUD operations for company management in admin panel. """ +import os import re import csv import logging @@ -15,7 +16,10 @@ from flask import render_template, request, redirect, url_for, flash, jsonify, R from flask_login import login_required, current_user from . import bp -from database import SessionLocal, Company, Category, User, Person, CompanyPerson, SystemRole +from database import ( + SessionLocal, Company, Category, User, Person, CompanyPerson, SystemRole, + CompanyWebsiteAnalysis, CompanySocialMedia, GBPAudit +) from utils.decorators import role_required # Logger @@ -94,7 +98,8 @@ def admin_companies(): current_status=status_filter, current_category=category_filter, current_quality=quality_filter, - search_query=search_query + search_query=search_query, + now=datetime.utcnow() ) finally: db.close() @@ -635,3 +640,117 @@ def company_settings(company_id): ) finally: db.close() + + +@bp.route('/companies//detail') +@login_required +@role_required(SystemRole.OFFICE_MANAGER) +def admin_company_detail(company_id): + """Admin company detail page with enrichment status and completeness score.""" + db = SessionLocal() + try: + company = db.query(Company).filter(Company.id == company_id).first() + if not company: + flash('Firma nie istnieje', 'error') + return redirect(url_for('admin.admin_companies')) + + # Users assigned to this company + users = db.query(User).filter(User.company_id == company_id).all() + + # --- Enrichment status --- + + # Registry data + registry_done = bool(company.ceidg_fetched_at or company.krs_fetched_at) + registry_source = None + registry_date = None + if company.krs_fetched_at: + registry_source = 'KRS' + registry_date = company.krs_fetched_at + elif company.ceidg_fetched_at: + registry_source = 'CEIDG' + registry_date = company.ceidg_fetched_at + + # Logo check + logo_path = os.path.join('static', 'img', 'companies', f'{company.slug}.webp') + logo_exists = os.path.isfile(logo_path) + + # SEO - latest website analysis + seo_analysis = db.query(CompanyWebsiteAnalysis).filter( + CompanyWebsiteAnalysis.company_id == company_id + ).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first() + + # Social media count and latest date + social_accounts = db.query(CompanySocialMedia).filter( + CompanySocialMedia.company_id == company_id + ).order_by(CompanySocialMedia.verified_at.desc()).all() + social_count = len(social_accounts) + social_latest_date = social_accounts[0].verified_at if social_accounts else None + + # GBP - latest audit + gbp_audit = db.query(GBPAudit).filter( + GBPAudit.company_id == company_id + ).order_by(GBPAudit.audit_date.desc()).first() + + enrichment = { + 'registry': { + 'done': registry_done, + 'source': registry_source, + 'date': registry_date, + 'has_krs': bool(company.krs), + 'has_nip': bool(company.nip), + }, + 'logo': { + 'done': logo_exists, + 'path': f'/static/img/companies/{company.slug}.webp' if logo_exists else None, + }, + 'seo': { + 'done': seo_analysis is not None, + 'date': seo_analysis.analyzed_at if seo_analysis else None, + 'score': seo_analysis.seo_overall_score if seo_analysis else None, + }, + 'social': { + 'done': social_count > 0, + 'count': social_count, + 'date': social_latest_date, + }, + 'gbp': { + 'done': gbp_audit is not None, + 'date': gbp_audit.audit_date if gbp_audit else None, + 'score': gbp_audit.completeness_score if gbp_audit else None, + }, + } + + # --- Completeness score (12 fields) --- + fields = { + 'NIP': bool(company.nip), + 'Adres': bool(company.address_city), + 'Telefon': bool(company.phone), + 'Email': bool(company.email), + 'Strona WWW': bool(company.website), + 'Opis': bool(company.description_short), + 'Kategoria': bool(company.category_id), + 'Logo': enrichment['logo']['done'], + 'Dane urzędowe': enrichment['registry']['done'], + 'Audyt SEO': enrichment['seo']['done'], + 'Audyt Social': enrichment['social']['done'], + 'Audyt GBP': enrichment['gbp']['done'], + } + + completeness = { + 'score': int(sum(fields.values()) / len(fields) * 100), + 'fields': fields, + 'total': len(fields), + 'filled': sum(fields.values()), + } + + logger.info(f"Admin {current_user.email} viewed company detail: {company.name} (ID: {company_id})") + + return render_template( + 'admin/company_detail.html', + company=company, + enrichment=enrichment, + completeness=completeness, + users=users, + ) + finally: + db.close() diff --git a/templates/admin/companies.html b/templates/admin/companies.html index 8537919..7ab4e5f 100644 --- a/templates/admin/companies.html +++ b/templates/admin/companies.html @@ -170,6 +170,20 @@ .company-name { font-weight: 500; color: var(--text-primary); + text-decoration: none; + } + + .company-name:hover { + text-decoration: underline; + color: var(--primary); + } + + .badge-new { + background: #FEF3C7; + color: #92400E; + font-size: 9px; + margin-left: 4px; + vertical-align: middle; } .company-city { @@ -522,7 +536,11 @@
- {{ company.name }} + {{ company.name }} + {% set age_days = ((now - company.created_at).total_seconds() / 86400)|int if company.created_at and now else 999 %} + {% if age_days <= 7 %} + NOWA + {% endif %} {% if company.admin_notes %} {% endif %} diff --git a/templates/admin/company_detail.html b/templates/admin/company_detail.html new file mode 100644 index 0000000..86657a9 --- /dev/null +++ b/templates/admin/company_detail.html @@ -0,0 +1,815 @@ +{% extends "base.html" %} + +{% block title %}{{ company.name }} - Panel Admin{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ +
+

+ {{ company.name }} + {{ company.status or 'pending' }} +

+ +
+
+ + +
+
+
{{ completeness.score }}%
+
Kompletność
+
+
+
+
+
+
+ {{ company.data_quality or 'basic' }} +
+
Jakość danych
+
+
+
{{ users|length }}
+
Użytkownicy
+
+
+
{{ company.created_at.strftime('%d.%m.%Y') if company.created_at else '---' }}
+
Utworzono
+
+
+ + +
+

Dane firmy

+
+
+ {% if enrichment.logo.path %} + Logo {{ company.name }} + {% else %} + + {% endif %} +
+
+

{{ company.name }}

+ {% if company.legal_name and company.legal_name != company.name %} +
{{ company.legal_name }}
+ {% endif %} +
+
+
+
+
+
NIP
+
{% if company.nip %}{{ company.nip }}{% else %}{% endif %}
+
+
+
KRS
+
{% if company.krs %}{{ company.krs }}{% else %}{% endif %}
+
+
+
REGON
+
{% if company.regon %}{{ company.regon }}{% else %}{% endif %}
+
+
+
Forma prawna
+
{{ company.legal_form or '' }}{%- if not company.legal_form %}{% endif %}
+
+
+
PKD
+
{% if company.pkd_code %}{{ company.pkd_code }}{% if company.pkd_description %} — {{ company.pkd_description }}{% endif %}{% else %}{% endif %}
+
+
+
Właściciel
+
{% if company.owner_first_name or company.owner_last_name %}{{ company.owner_first_name or '' }} {{ company.owner_last_name or '' }}{% else %}{% endif %}
+
+
+
+
+
Adres
+
+ {% if company.address_street or company.address_city %} + {{ company.address_street or '' }}{% if company.address_street and company.address_city %}, {% endif %} + {{ company.address_postal or '' }} {{ company.address_city or '' }} + {% else %} + + {% endif %} +
+
+
+
Email
+
{% if company.email %}{{ company.email }}{% else %}{% endif %}
+
+
+
Telefon
+
{{ company.phone or '' }}{%- if not company.phone %}{% endif %}
+
+
+
Strona WWW
+
{% if company.website %}{{ company.website }}{% else %}{% endif %}
+
+
+
Kategoria
+
{{ company.category.name if company.category else '' }}{%- if not company.category %}{% endif %}
+
+
+
Opis
+
{% if company.description_short %}{{ company.description_short[:200] }}{% if company.description_short|length > 200 %}...{% endif %}{% else %}{% endif %}
+
+
+
+ {% if company.admin_notes %} +
+
Notatki administracyjne
+
{{ company.admin_notes }}
+
+ {% endif %} +
+ + +
+

Workflow uzbrajania firmy

+ +
+ +
+ +
+ +
+
+ +

Dane urzędowe

+
+
+ {% if enrichment.registry.done %} + + Wykonano{% if enrichment.registry.date %} {{ enrichment.registry.date.strftime('%d.%m.%Y') }}{% endif %} + {% else %} + + Nie wykonano + {% endif %} +
+ {% if enrichment.registry.source %} +
Źródło: {{ enrichment.registry.source }}
+ {% endif %} + +
+ + +
+
+ +

Logo firmy

+
+
+ {% if enrichment.logo.done %} + + Pobrano + {% else %} + + Nie pobrano + {% endif %} +
+ +
+ + +
+
+ +

Audyt SEO

+
+
+ {% if enrichment.seo.done %} + + Wykonano{% if enrichment.seo.date %} {{ enrichment.seo.date.strftime('%d.%m.%Y') }}{% endif %} + {% else %} + + Nie wykonano + {% endif %} +
+ {% if enrichment.seo.score is not none and enrichment.seo.score is defined %} +
Wynik: {{ enrichment.seo.score }}/100
+ {% endif %} + +
+ + +
+
+ +

Audyt Social Media

+
+
+ {% if enrichment.social.done %} + + Wykonano ({{ enrichment.social.count }} profili) + {% else %} + + Nie wykonano + {% endif %} +
+ +
+ + +
+
+ +

Audyt GBP

+
+
+ {% if enrichment.gbp.done %} + + Wykonano{% if enrichment.gbp.date %} {{ enrichment.gbp.date.strftime('%d.%m.%Y') }}{% endif %} + {% else %} + + Nie wykonano + {% endif %} +
+ {% if enrichment.gbp.score is not none and enrichment.gbp.score is defined %} +
Wynik: {{ enrichment.gbp.score }}/100
+ {% endif %} + +
+
+
+ + +
+

Lista kompletności ({{ completeness.filled }}/{{ completeness.total }})

+
+ {% for field_name, is_filled in completeness.fields.items() %} +
+ {% if is_filled %} + + {% else %} + + {% endif %} + {{ field_name }} +
+ {% endfor %} +
+
+
+ + +
+{% endblock %} + +{% block extra_js %} + var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + function showToast(message, type) { + var container = document.getElementById('toastContainer'); + var toast = document.createElement('div'); + toast.className = 'toast ' + (type || 'success'); + toast.innerHTML = '' + message + '' + + ''; + container.appendChild(toast); + setTimeout(function() { + toast.style.animation = 'slideOut 0.3s ease forwards'; + setTimeout(function() { toast.remove(); }, 300); + }, 5000); + } + + function runEnrichAction(btn, url, body) { + var original = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ' Pobieram...'; + return fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken}, + body: JSON.stringify(body || {}) + }) + .then(function(resp) { return resp.json(); }) + .then(function(data) { + if (data.success) { + showToast(data.message || 'Gotowe!', 'success'); + btn.innerHTML = ' Gotowe'; + return true; + } else { + showToast(data.error || 'Wystąpił błąd', 'error'); + btn.innerHTML = original; + btn.disabled = false; + return false; + } + }) + .catch(function() { + showToast('Błąd połączenia z serwerem', 'error'); + btn.innerHTML = original; + btn.disabled = false; + return false; + }); + } + + function fetchRegistry() { + var btn = document.getElementById('btn-registry'); + return runEnrichAction(btn, '/api/company/{{ company.id }}/enrich-registry', {}); + } + + function fetchLogo() { + var btn = document.getElementById('btn-logo'); + return runEnrichAction(btn, '/api/company/{{ company.id }}/fetch-logo', {action: 'fetch'}); + } + + function runSeoAudit() { + var btn = document.getElementById('btn-seo'); + return runEnrichAction(btn, '/api/seo/audit', {company_id: {{ company.id }}}); + } + + function runSocialAudit() { + var btn = document.getElementById('btn-social'); + return runEnrichAction(btn, '/api/social/audit', {company_id: {{ company.id }}}); + } + + function runGbpAudit() { + var btn = document.getElementById('btn-gbp'); + return runEnrichAction(btn, '/api/gbp/audit', {company_id: {{ company.id }}, fetch_google: true}); + } + + function armCompany() { + var masterBtn = document.getElementById('btn-arm'); + masterBtn.disabled = true; + masterBtn.innerHTML = ' Uzbrajam firmę...'; + + var steps = [ + {id: 'btn-registry', fn: fetchRegistry, skip: {{ 'true' if enrichment.registry.done else 'false' }}, requires: {{ 'true' if company.nip else 'false' }} }, + {id: 'btn-logo', fn: fetchLogo, skip: {{ 'true' if enrichment.logo.done else 'false' }}, requires: {{ 'true' if company.website else 'false' }} }, + {id: 'btn-seo', fn: runSeoAudit, skip: {{ 'true' if enrichment.seo.done else 'false' }}, requires: {{ 'true' if company.website else 'false' }} }, + {id: 'btn-social', fn: runSocialAudit, skip: {{ 'true' if enrichment.social.done else 'false' }}, requires: true }, + {id: 'btn-gbp', fn: runGbpAudit, skip: {{ 'true' if enrichment.gbp.done else 'false' }}, requires: true } + ]; + + var pending = steps.filter(function(s) { return !s.skip && s.requires; }); + + if (pending.length === 0) { + showToast('Wszystkie kroki zostały już wykonane!', 'success'); + masterBtn.disabled = false; + masterBtn.innerHTML = ' Uzbrój firmę'; + return; + } + + var idx = 0; + function runNext() { + if (idx >= pending.length) { + showToast('Uzbrajanie zakończone! Odświeżam stronę...', 'success'); + setTimeout(function() { location.reload(); }, 2000); + return; + } + var step = pending[idx]; + idx++; + masterBtn.innerHTML = ' Krok ' + idx + '/' + pending.length + '...'; + step.fn().then(function() { + runNext(); + }); + } + runNext(); + } +{% endblock %}