feat: add admin company detail page with enrichment workflow
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

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 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-20 13:29:00 +01:00
parent 0e96dd80fa
commit ff35ef6b43
3 changed files with 955 additions and 3 deletions

View File

@ -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/<int:company_id>/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()

View File

@ -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 @@
<td>
<div class="company-info">
<div style="display: flex; align-items: center; gap: 6px;">
<a href="{{ url_for('company_detail_by_slug', slug=company.slug) }}" class="company-name" target="_blank">{{ company.name }}</a>
<a href="/admin/companies/{{ company.id }}/detail" class="company-name">{{ company.name }}</a>
{% set age_days = ((now - company.created_at).total_seconds() / 86400)|int if company.created_at and now else 999 %}
{% if age_days <= 7 %}
<span class="badge badge-new">NOWA</span>
{% endif %}
{% if company.admin_notes %}
<span title="{{ company.admin_notes[:100] }}..." style="cursor: help; color: #f59e0b; font-size: 14px;">&#9998;</span>
{% endif %}

View File

@ -0,0 +1,815 @@
{% extends "base.html" %}
{% block title %}{{ company.name }} - Panel Admin{% endblock %}
{% block extra_css %}
<style>
.detail-header {
margin-bottom: var(--spacing-xl);
}
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.breadcrumb a {
color: var(--primary);
text-decoration: none;
}
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb svg {
width: 14px;
height: 14px;
color: var(--text-secondary);
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--spacing-md);
}
.header-row h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin: 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
flex-shrink: 0;
}
.btn-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-primary);
}
.btn-link:hover { background: var(--background); }
.btn-link svg { width: 16px; height: 16px; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.badge-active { background: #D1FAE5; color: #065F46; }
.badge-pending { background: #FEF3C7; color: #92400E; }
.badge-inactive { background: #E5E7EB; color: #374151; }
.badge-archived { background: #FEE2E2; color: #991B1B; }
.badge-basic { background: #E5E7EB; color: #374151; }
.badge-enhanced { background: #DBEAFE; color: #1D4ED8; }
.badge-complete { background: #D1FAE5; color: #065F46; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
text-align: center;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: 700;
color: var(--primary);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.progress-bar-wrap {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
margin-top: var(--spacing-sm);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xl);
}
.info-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.logo-box {
width: 80px;
height: 80px;
border-radius: var(--radius-lg);
overflow: hidden;
flex-shrink: 0;
background: var(--background);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
}
.logo-box img {
width: 100%;
height: 100%;
object-fit: contain;
}
.logo-box svg {
width: 32px;
height: 32px;
color: var(--text-secondary);
opacity: 0.4;
}
.info-field {
margin-bottom: var(--spacing-md);
}
.info-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: 2px;
}
.info-value {
color: var(--text-primary);
word-break: break-word;
}
.info-value a {
color: var(--primary);
text-decoration: none;
}
.info-value a:hover { text-decoration: underline; }
.info-empty { color: var(--text-secondary); opacity: 0.5; }
/* Workflow section */
.master-btn-wrap {
margin-bottom: var(--spacing-lg);
}
.btn-master {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-xl);
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
border: none;
border-radius: var(--radius-lg);
font-size: var(--font-size-lg);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-md);
}
.btn-master:hover { opacity: 0.9; box-shadow: var(--shadow-lg); }
.btn-master:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-master svg { width: 22px; height: 22px; }
.actions-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-lg);
}
.action-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border-left: 4px solid var(--border);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.action-card.done { border-left-color: var(--success); }
.action-card-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.action-card-header svg {
width: 24px;
height: 24px;
color: var(--primary);
flex-shrink: 0;
}
.action-card-header h3 {
font-size: var(--font-size-base);
color: var(--text-primary);
margin: 0;
}
.action-status {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.status-dot.green { background: var(--success); }
.status-dot.gray { background: var(--border); }
.action-score {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.btn-action {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
margin-top: auto;
}
.btn-action:hover { opacity: 0.9; }
.btn-action:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-action svg { width: 16px; height: 16px; }
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Completeness checklist */
.checklist-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xs) var(--spacing-xl);
}
.checklist-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border);
font-size: var(--font-size-sm);
}
.checklist-item svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.check-ok { color: var(--success); }
.check-missing { color: var(--error); }
/* Toast */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--surface);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-width: 280px;
max-width: 400px;
animation: slideIn 0.3s ease;
border-left: 4px solid var(--primary);
}
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
.toast-message { flex: 1; color: var(--text-primary); }
.toast-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@media (max-width: 768px) {
.header-row { flex-direction: column; }
.info-grid { grid-template-columns: 1fr; }
.actions-grid { grid-template-columns: 1fr; }
.checklist-grid { grid-template-columns: 1fr; }
.header-actions { width: 100%; justify-content: flex-end; }
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<!-- 1. Header -->
<div class="detail-header">
<div class="breadcrumb">
<a href="{{ url_for('admin.admin_companies') }}">Firmy</a>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
<span>{{ company.name }}</span>
</div>
<div class="header-row">
<h1>
{{ company.name }}
<span class="badge badge-{{ company.status or 'pending' }}">{{ company.status or 'pending' }}</span>
</h1>
<div class="header-actions">
<a href="{{ url_for('admin.company_settings', company_id=company.id) }}" class="btn-link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>
Ustawienia
</a>
<a href="{{ url_for('admin.admin_companies') }}" class="btn-link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Lista firm
</a>
</div>
</div>
</div>
<!-- 2. Stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" style="color: {% if completeness.score < 30 %}var(--error){% elif completeness.score < 70 %}var(--warning){% else %}var(--success){% endif %};">{{ completeness.score }}%</div>
<div class="stat-label">Kompletność</div>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" style="width: {{ completeness.score }}%; background: {% if completeness.score < 30 %}var(--error){% elif completeness.score < 70 %}var(--warning){% else %}var(--success){% endif %};"></div>
</div>
</div>
<div class="stat-card">
<div class="stat-value">
<span class="badge badge-{{ company.data_quality or 'basic' }}" style="font-size: var(--font-size-lg);">{{ company.data_quality or 'basic' }}</span>
</div>
<div class="stat-label">Jakość danych</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ users|length }}</div>
<div class="stat-label">Użytkownicy</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ company.created_at.strftime('%d.%m.%Y') if company.created_at else '---' }}</div>
<div class="stat-label">Utworzono</div>
</div>
</div>
<!-- 3. Company info -->
<div class="section">
<h2>Dane firmy</h2>
<div class="info-header">
<div class="logo-box">
{% if enrichment.logo.path %}
<img src="{{ enrichment.logo.path }}" alt="Logo {{ company.name }}">
{% else %}
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z"/></svg>
{% endif %}
</div>
<div>
<h3 style="margin: 0; color: var(--text-primary);">{{ company.name }}</h3>
{% if company.legal_name and company.legal_name != company.name %}
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ company.legal_name }}</div>
{% endif %}
</div>
</div>
<div class="info-grid">
<div>
<div class="info-field">
<div class="info-label">NIP</div>
<div class="info-value">{% if company.nip %}<span style="font-family: monospace;">{{ company.nip }}</span>{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">KRS</div>
<div class="info-value">{% if company.krs %}<span style="font-family: monospace;">{{ company.krs }}</span>{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">REGON</div>
<div class="info-value">{% if company.regon %}<span style="font-family: monospace;">{{ company.regon }}</span>{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Forma prawna</div>
<div class="info-value">{{ company.legal_form or '' }}{%- if not company.legal_form %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">PKD</div>
<div class="info-value">{% if company.pkd_code %}{{ company.pkd_code }}{% if company.pkd_description %} &mdash; {{ company.pkd_description }}{% endif %}{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Właściciel</div>
<div class="info-value">{% if company.owner_first_name or company.owner_last_name %}{{ company.owner_first_name or '' }} {{ company.owner_last_name or '' }}{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
</div>
<div>
<div class="info-field">
<div class="info-label">Adres</div>
<div class="info-value">
{% 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 %}
<span class="info-empty">&mdash;</span>
{% endif %}
</div>
</div>
<div class="info-field">
<div class="info-label">Email</div>
<div class="info-value">{% if company.email %}<a href="mailto:{{ company.email }}">{{ company.email }}</a>{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Telefon</div>
<div class="info-value">{{ company.phone or '' }}{%- if not company.phone %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Strona WWW</div>
<div class="info-value">{% if company.website %}<a href="{{ company.website }}" target="_blank" rel="noopener">{{ company.website }}</a>{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Kategoria</div>
<div class="info-value">{{ company.category.name if company.category else '' }}{%- if not company.category %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
<div class="info-field">
<div class="info-label">Opis</div>
<div class="info-value">{% if company.description_short %}{{ company.description_short[:200] }}{% if company.description_short|length > 200 %}...{% endif %}{% else %}<span class="info-empty">&mdash;</span>{% endif %}</div>
</div>
</div>
</div>
{% if company.admin_notes %}
<div style="margin-top: var(--spacing-lg); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
<div class="info-label" style="color: var(--warning); font-weight: 600;">Notatki administracyjne</div>
<div class="info-value" style="white-space: pre-line;">{{ company.admin_notes }}</div>
</div>
{% endif %}
</div>
<!-- 4. Workflow -->
<div class="section">
<h2>Workflow uzbrajania firmy</h2>
<div class="master-btn-wrap">
<button id="btn-arm" class="btn-master" onclick="armCompany()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
Uzbrój firmę
</button>
</div>
<div class="actions-grid">
<!-- Registry -->
<div class="action-card {{ 'done' if enrichment.registry.done else '' }}">
<div class="action-card-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
<h3>Dane urzędowe</h3>
</div>
<div class="action-status">
{% if enrichment.registry.done %}
<span class="status-dot green"></span>
Wykonano{% if enrichment.registry.date %} {{ enrichment.registry.date.strftime('%d.%m.%Y') }}{% endif %}
{% else %}
<span class="status-dot gray"></span>
Nie wykonano
{% endif %}
</div>
{% if enrichment.registry.source %}
<div class="action-score">Źródło: {{ enrichment.registry.source }}</div>
{% endif %}
<button id="btn-registry" class="btn-action" onclick="fetchRegistry()" {{ 'disabled' if not company.nip }}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Pobierz dane
</button>
</div>
<!-- Logo -->
<div class="action-card {{ 'done' if enrichment.logo.done else '' }}">
<div class="action-card-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<h3>Logo firmy</h3>
</div>
<div class="action-status">
{% if enrichment.logo.done %}
<span class="status-dot green"></span>
Pobrano
{% else %}
<span class="status-dot gray"></span>
Nie pobrano
{% endif %}
</div>
<button id="btn-logo" class="btn-action" onclick="fetchLogo()" {{ 'disabled' if not company.website }}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Pobierz logo
</button>
</div>
<!-- SEO -->
<div class="action-card {{ 'done' if enrichment.seo.done else '' }}">
<div class="action-card-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<h3>Audyt SEO</h3>
</div>
<div class="action-status">
{% if enrichment.seo.done %}
<span class="status-dot green"></span>
Wykonano{% if enrichment.seo.date %} {{ enrichment.seo.date.strftime('%d.%m.%Y') }}{% endif %}
{% else %}
<span class="status-dot gray"></span>
Nie wykonano
{% endif %}
</div>
{% if enrichment.seo.score is not none and enrichment.seo.score is defined %}
<div class="action-score">Wynik: {{ enrichment.seo.score }}/100</div>
{% endif %}
<button id="btn-seo" class="btn-action" onclick="runSeoAudit()" {{ 'disabled' if not company.website }}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Uruchom audyt
</button>
</div>
<!-- Social Media -->
<div class="action-card {{ 'done' if enrichment.social.done else '' }}">
<div class="action-card-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<h3>Audyt Social Media</h3>
</div>
<div class="action-status">
{% if enrichment.social.done %}
<span class="status-dot green"></span>
Wykonano ({{ enrichment.social.count }} profili)
{% else %}
<span class="status-dot gray"></span>
Nie wykonano
{% endif %}
</div>
<button id="btn-social" class="btn-action" onclick="runSocialAudit()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Uruchom audyt
</button>
</div>
<!-- GBP -->
<div class="action-card {{ 'done' if enrichment.gbp.done else '' }}">
<div class="action-card-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<h3>Audyt GBP</h3>
</div>
<div class="action-status">
{% if enrichment.gbp.done %}
<span class="status-dot green"></span>
Wykonano{% if enrichment.gbp.date %} {{ enrichment.gbp.date.strftime('%d.%m.%Y') }}{% endif %}
{% else %}
<span class="status-dot gray"></span>
Nie wykonano
{% endif %}
</div>
{% if enrichment.gbp.score is not none and enrichment.gbp.score is defined %}
<div class="action-score">Wynik: {{ enrichment.gbp.score }}/100</div>
{% endif %}
<button id="btn-gbp" class="btn-action" onclick="runGbpAudit()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Uruchom audyt
</button>
</div>
</div>
</div>
<!-- 5. Completeness checklist -->
<div class="section">
<h2>Lista kompletności ({{ completeness.filled }}/{{ completeness.total }})</h2>
<div class="checklist-grid">
{% for field_name, is_filled in completeness.fields.items() %}
<div class="checklist-item">
{% if is_filled %}
<svg class="check-ok" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{% else %}
<svg class="check-missing" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{% endif %}
<span>{{ field_name }}</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
{% 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 = '<span class="toast-message">' + message + '</span>' +
'<button class="toast-close" onclick="this.parentElement.remove()">' +
'<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>' +
'</button>';
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 = '<span class="spinner"></span> 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 = '<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="width:16px;height:16px"><path d="M5 13l4 4L19 7"/></svg> 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 = '<span class="spinner"></span> 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 = '<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="width:22px;height:22px"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg> 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 = '<span class="spinner"></span> Krok ' + idx + '/' + pending.length + '...';
step.fn().then(function() {
runNext();
});
}
runNext();
}
{% endblock %}