nordabiz/templates/gbp_audit.html
Maciej Pienczyn 555cb99c86
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
feat: capture and display Google account email for OAuth connections
After token exchange, fetches Google userinfo to save the email and
name of the Google account used for authorization. Displays this info
on the GBP audit page so users know which account to reconnect with.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 07:32:43 +01:00

2855 lines
134 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Audyt Google Business Profile - {{ company.name }} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.audit-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-md);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.audit-header-info h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.audit-header-info p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.data-source-info {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--info-light, #e0f2fe);
border-radius: var(--radius);
font-size: var(--font-size-sm);
color: var(--info, #0284c7);
}
.data-source-info svg {
flex-shrink: 0;
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
/* Score Display */
.score-section {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
@media (max-width: 768px) {
.score-section {
grid-template-columns: 1fr;
text-align: center;
}
}
.score-circle {
width: 180px;
height: 180px;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background: conic-gradient(
var(--score-color, var(--secondary)) calc(var(--score-percent, 0) * 3.6deg),
#e2e8f0 0deg
);
margin: 0 auto;
}
.score-circle::before {
content: '';
position: absolute;
width: 150px;
height: 150px;
border-radius: 50%;
background: var(--surface);
}
.score-value {
position: relative;
z-index: 1;
font-size: 3rem;
font-weight: 700;
line-height: 1;
}
/* Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green */
.score-value.score-excellent { color: #10b981; }
.score-value.score-good { color: #84cc16; }
.score-value.score-average { color: #f59e0b; }
.score-value.score-needs-work { color: #f97316; }
.score-value.score-poor { color: #ef4444; }
.score-label {
position: relative;
z-index: 1;
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.score-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.score-category {
font-size: var(--font-size-xl);
font-weight: 600;
margin-bottom: var(--spacing-sm);
}
/* Unified 5-level color scale */
.score-category.excellent { color: #10b981; }
.score-category.good { color: #84cc16; }
.score-category.average { color: #f59e0b; }
.score-category.needs-work { color: #f97316; }
.score-category.poor { color: #ef4444; }
.score-description {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: var(--spacing-md);
}
.audit-meta {
display: flex;
gap: var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
flex-wrap: wrap;
}
.audit-meta-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Fields Section */
.fields-section {
margin-bottom: var(--spacing-md);
}
.section-title {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.fields-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-md);
}
.field-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
box-shadow: var(--shadow-sm);
border-left: 4px solid var(--border);
transition: var(--transition);
}
.field-card:hover {
box-shadow: var(--shadow);
}
.field-card.complete {
border-left-color: var(--success);
}
.field-card.partial {
border-left-color: var(--warning);
}
.field-card.missing {
border-left-color: var(--error);
}
.field-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-sm);
}
.field-name {
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.field-status-badge {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: var(--radius-sm);
text-transform: uppercase;
}
.field-status-badge.complete {
background: #dcfce7;
color: #166534;
}
.field-status-badge.partial {
background: #fef3c7;
color: #92400e;
}
.field-status-badge.missing {
background: #fee2e2;
color: #991b1b;
}
.field-value {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
word-break: break-word;
}
.field-score {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.field-score-bar {
flex: 1;
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.field-score-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.field-score-fill.complete { background: var(--success); }
.field-score-fill.partial { background: var(--warning); }
.field-score-fill.missing { background: var(--error); }
.field-score-text {
font-size: var(--font-size-sm);
color: var(--text-secondary);
min-width: 60px;
text-align: right;
}
.field-scoring-info.reviews-scoring {
margin-top: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
.scoring-message {
display: block;
}
.scoring-message.success {
color: var(--success);
}
.scoring-message.hint {
color: var(--warning);
}
.scoring-message.progress {
color: var(--primary);
}
.scoring-breakdown-line {
margin-top: var(--spacing-xs);
font-size: var(--font-size-xs);
color: var(--text-tertiary);
font-family: monospace;
}
/* Recommendations Section */
.recommendations-section {
margin-bottom: var(--spacing-md);
}
.recommendation-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.recommendation-item {
background: var(--surface);
border-radius: var(--radius);
padding: var(--spacing-md);
box-shadow: var(--shadow-sm);
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
transition: var(--transition);
}
.recommendation-item:hover {
box-shadow: var(--shadow);
}
.recommendation-priority {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.recommendation-priority.high {
background: #fee2e2;
color: var(--error);
}
.recommendation-priority.medium {
background: #fef3c7;
color: var(--warning);
}
.recommendation-priority.low {
background: #dbeafe;
color: var(--primary);
}
.recommendation-content {
flex: 1;
}
.recommendation-field {
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
text-transform: capitalize;
}
.recommendation-text {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.recommendation-impact {
flex-shrink: 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Educational Info Section */
.info-section {
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-md);
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border: 1px solid #bfdbfe;
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
}
.info-section-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.info-section-header svg {
width: 24px;
height: 24px;
color: #2563eb;
flex-shrink: 0;
}
.info-section-header h3 {
font-size: var(--font-size-lg);
color: #1e40af;
margin: 0;
}
.info-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.info-card {
background: white;
border-radius: var(--radius);
padding: var(--spacing-md);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.info-card-icon {
width: 40px;
height: 40px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-sm);
}
.info-card-icon.search {
background: #fef3c7;
color: #d97706;
}
.info-card-icon.maps {
background: #dcfce7;
color: #16a34a;
}
.info-card-icon.manage {
background: #e0e7ff;
color: #4f46e5;
}
.info-card h4 {
font-size: var(--font-size-md);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.info-card p {
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
}
.info-card ul {
margin: var(--spacing-sm) 0 0 0;
padding-left: var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.info-card ul li {
margin-bottom: var(--spacing-xs);
}
.info-summary {
background: white;
border-radius: var(--radius);
padding: var(--spacing-md);
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.info-summary svg {
width: 20px;
height: 20px;
color: #2563eb;
flex-shrink: 0;
margin-top: 2px;
}
.info-summary p {
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.6;
margin: 0;
}
.info-summary strong {
color: var(--text-primary);
}
.info-summary a {
color: #2563eb;
text-decoration: none;
font-weight: 500;
}
.info-summary a:hover {
text-decoration: underline;
}
/* No Audit State */
.no-audit-state {
text-align: center;
padding: var(--spacing-2xl);
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.no-audit-state svg {
width: 80px;
height: 80px;
color: var(--text-secondary);
opacity: 0.5;
margin-bottom: var(--spacing-md);
}
.no-audit-state h2 {
font-size: var(--font-size-xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.no-audit-state p {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
/* Loading State */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 1000;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing-lg);
}
.loading-overlay.active {
display: flex;
}
.loading-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 400px;
width: 90%;
text-align: center;
}
.loading-header {
margin-bottom: var(--spacing-lg);
}
.loading-header h3 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.loading-header p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.loading-steps {
text-align: left;
margin-bottom: var(--spacing-lg);
}
.loading-step {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border-light);
}
.loading-step.step-detail {
padding-left: var(--spacing-lg);
font-size: var(--font-size-xs);
}
.loading-step:last-child {
border-bottom: none;
}
.step-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-icon.pending {
color: var(--text-tertiary);
}
.step-icon.in_progress {
color: var(--primary);
}
.step-icon.complete {
color: var(--success);
}
.step-icon.error {
color: var(--error);
}
.step-icon.warning {
color: var(--warning);
}
.step-icon.missing {
color: var(--text-tertiary);
opacity: 0.6;
}
.step-icon.skipped {
color: var(--text-tertiary);
opacity: 0.5;
}
.step-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.step-text {
flex: 1;
font-size: var(--font-size-sm);
}
.step-text.pending {
color: var(--text-tertiary);
}
.step-text.in_progress {
color: var(--text-primary);
font-weight: 500;
}
.step-text.complete {
color: var(--text-secondary);
}
.step-text.error {
color: var(--error);
}
.step-text.missing {
color: var(--text-tertiary);
font-style: italic;
}
.step-text.skipped {
color: var(--text-tertiary);
opacity: 0.6;
}
.step-text.warning {
color: var(--warning);
}
/* Source tag styling */
.source-tag {
display: inline-block;
font-size: 9px;
font-weight: 600;
padding: 1px 5px;
border-radius: 3px;
margin-left: 4px;
text-transform: uppercase;
vertical-align: middle;
}
.source-tag.google {
background: #4285f4;
color: white;
}
.source-tag.nordabiz {
background: var(--primary);
color: white;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: var(--text-secondary);
font-size: var(--font-size-lg);
}
/* Legend */
.legend {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 2px;
}
.legend-dot.complete { background: #dcfce7; border: 1px solid #166534; }
.legend-dot.partial { background: #fef3c7; border: 1px solid #92400e; }
.legend-dot.missing { background: #fee2e2; border: 1px solid #991b1b; }
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
.breadcrumb a {
color: var(--primary);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb-separator {
color: var(--border);
}
/* Modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 480px;
width: 90%;
box-shadow: var(--shadow-lg);
animation: modalSlideIn 0.2s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.modal-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.modal-icon.success {
background: #dcfce7;
color: #16a34a;
}
.modal-icon.info {
background: #dbeafe;
color: #2563eb;
}
.modal-icon.warning {
background: #fef3c7;
color: #d97706;
}
.modal-title {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
}
.modal-body {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: var(--spacing-lg);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
/* Field icons */
.field-icon {
width: 16px;
height: 16px;
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 768px) {
.audit-header {
flex-direction: column;
}
.header-actions {
width: 100%;
justify-content: center;
}
.fields-grid {
grid-template-columns: 1fr;
}
.recommendation-item {
flex-direction: column;
}
.recommendation-impact {
align-self: flex-start;
}
}
</style>
{% endblock %}
{% block content %}
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="{{ url_for('index') }}">Firmy</a>
<span class="breadcrumb-separator">/</span>
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
<span class="breadcrumb-separator">/</span>
<span>Audyt GBP</span>
</div>
<div class="audit-header">
<div class="audit-header-info">
<h1>Audyt Google Business Profile</h1>
<p>{{ company.name }}</p>
<div class="data-source-info">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Analiza kompletności wizytówki Google dla lokalnego SEO</span>
</div>
</div>
<div class="header-actions">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn btn-outline btn-sm">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Profil firmy
</a>
{% if audit and (audit.google_maps_url or audit.google_place_id) %}
{% set gbp_url = audit.google_maps_url if audit.google_maps_url else 'https://www.google.com/maps/search/?api=1&query=Google&query_place_id=' ~ audit.google_place_id %}
<a href="{{ gbp_url }}" target="_blank" rel="noopener" class="btn btn-outline btn-sm" style="color: #4285f4; border-color: #4285f4;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
Zobacz wizytówkę Google
</a>
{% endif %}
{% if places_data and places_data.maps_links %}
{% if places_data.maps_links.writeAReviewUri %}
<a href="{{ places_data.maps_links.writeAReviewUri }}" target="_blank" rel="noopener" class="btn btn-sm" style="background: #10b981; color: white; border: none;">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
Poproś o opinię
</a>
{% endif %}
{% if places_data.maps_links.directionsUri %}
<a href="{{ places_data.maps_links.directionsUri }}" target="_blank" rel="noopener" class="btn btn-outline btn-sm">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
</svg>
Pokaż trasę
</a>
{% endif %}
{% endif %}
{% if can_audit %}
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Uruchom audyt
</button>
{% endif %}
</div>
</div>
{# GBP Console Connection Status — at the top for visibility #}
{% if gbp_connection and gbp_connection.connected %}
{% if gbp_connection.is_active and not gbp_connection.is_expired %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); background: rgba(34, 197, 94, 0.06); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: var(--radius-lg); margin-bottom: var(--spacing-md);">
<span style="font-size: 22px;"></span>
<div style="flex: 1;">
<strong style="color: #16a34a;">Konsola Google Business Profile połączona</strong>
<p style="margin: 2px 0 0; font-size: var(--font-size-xs); color: var(--text-secondary);">
Dostępne są pełne dane: wyświetlenia, wyszukiwania, kliknięcia i interakcje klientów.
{% if gbp_connection.google_email %}Konto: <strong>{{ gbp_connection.google_email }}</strong>.{% endif %}
{% if gbp_connection.created_at %}Połączone od {{ gbp_connection.created_at.strftime('%d.%m.%Y') }}.{% endif %}
</p>
</div>
<a href="{{ url_for('auth.konto_integracje') }}" class="btn btn-outline btn-sm" style="white-space: nowrap;">Zarządzaj</a>
</div>
{% else %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); background: rgba(245, 158, 11, 0.06); border: 1px solid rgba(245, 158, 11, 0.25); border-radius: var(--radius-lg); margin-bottom: var(--spacing-md);">
<span style="font-size: 22px;"></span>
<div style="flex: 1;">
<strong style="color: #d97706;">Połączenie z konsolą GBP wygasło</strong>
<p style="margin: 2px 0 0; font-size: var(--font-size-xs); color: var(--text-secondary);">
Autoryzacja Google wygasła{% if gbp_connection.expires_at %} {{ gbp_connection.expires_at.strftime('%d.%m.%Y') }}{% endif %}.
{% if gbp_connection.google_email %}Ostatnio użyte konto: <strong>{{ gbp_connection.google_email }}</strong>.{% endif %}
Połącz ponownie tym samym kontem, aby przywrócić pełne statystyki.
</p>
</div>
<a href="{{ url_for('auth.konto_integracje') }}" class="btn btn-sm" style="background: #d97706; color: white; border: none; white-space: nowrap;">Połącz ponownie</a>
</div>
{% endif %}
{% else %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); background: rgba(107, 114, 128, 0.04); border: 1px solid var(--border); border-radius: var(--radius-lg); margin-bottom: var(--spacing-md);">
<span style="font-size: 22px; color: var(--text-tertiary);"></span>
<div style="flex: 1;">
<strong style="color: var(--text-secondary);">Konsola Google Business Profile niepołączona</strong>
<p style="margin: 2px 0 0; font-size: var(--font-size-xs); color: var(--text-secondary);">
Dane audytu pochodzą wyłącznie z publicznego API Google Places. Po połączeniu konta będą dostępne dodatkowe statystyki: wyświetlenia w Google, kliknięcia, połączenia telefoniczne i nawigacje.
</p>
</div>
<a href="{{ url_for('auth.konto_integracje') }}" class="btn btn-primary btn-sm" style="white-space: nowrap;">Połącz konto</a>
</div>
{% endif %}
{% if audit %}
<!-- Score Section -->
{# Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green #}
{% set score = audit.completeness_score %}
<div class="score-section">
<div class="score-circle" style="--score-percent: {{ score }}; --score-color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
<span class="score-value" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">{{ score }}</span>
<span class="score-label">/ 100</span>
</div>
<div class="score-details">
{% if places_data and places_data.open_now is not none %}
<div style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; margin-bottom: 8px; {% if places_data.open_now %}background: #d1fae5; color: #065f46;{% else %}background: #fee2e2; color: #991b1b;{% endif %}">
<span style="width: 8px; height: 8px; border-radius: 50%; {% if places_data.open_now %}background: #10b981;{% else %}background: #ef4444;{% endif %}"></span>
{% if places_data.open_now %}Otwarte{% else %}Zamknięte{% endif %}
<span style="font-weight: 400; font-size: 11px; opacity: 0.7;">(na moment audytu)</span>
</div>
{% endif %}
<div class="score-category" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
{% if score >= 90 %}
Doskonały profil GBP
{% elif score >= 70 %}
Dobry profil GBP
{% elif score >= 50 %}
Przeciętny profil GBP
{% elif score >= 30 %}
Profil wymaga uzupełnienia
{% else %}
Słaby profil GBP
{% endif %}
</div>
<p class="score-description">
{% if audit.completeness_score >= 90 %}
Twoja wizytówka Google jest bardzo dobrze zoptymalizowana. Utrzymaj wysoki standard i monitoruj opinie klientów.
{% elif audit.completeness_score >= 70 %}
Profil jest w dobrym stanie, ale są obszary do poprawy. Skupienie się na rekomendacjach zwiększy widoczność.
{% elif audit.completeness_score >= 50 %}
Wizytówka wymaga uzupełnienia. Wdrożenie poniższych rekomendacji znacząco poprawi lokalne SEO.
{% else %}
Wizytówka jest niekompletna i traci potencjalnych klientów. Priorytetowo uzupełnij brakujące informacje.
{% endif %}
</p>
<div class="audit-meta">
<div class="audit-meta-item">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Ostatni audyt: {{ audit.audit_date.strftime('%d.%m.%Y %H:%M') if audit.audit_date else 'Brak danych' }}</span>
</div>
{% if audit.review_count %}
<div class="audit-meta-item">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
<span>{{ audit.review_count }} opinii{% if audit.average_rating %} ({{ audit.average_rating }}/5){% endif %}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% if places_data and (places_data.primary_type or places_data.editorial_summary or places_data.price_level or places_data.google_business_status or places_data.google_website or places_data.google_types) %}
<!-- Google Places Enrichment Data -->
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#4285f4">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
Dane z Google Places
</h3>
{# Business status badge #}
{% if places_data.google_business_status %}
<div style="margin-bottom: var(--spacing-sm);">
{% set bs = places_data.google_business_status %}
<span style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
{% if bs == 'OPERATIONAL' %}background: #d1fae5; color: #065f46;
{% elif bs == 'CLOSED_TEMPORARILY' %}background: #fef3c7; color: #92400e;
{% else %}background: #fee2e2; color: #991b1b;{% endif %}">
<span style="width: 8px; height: 8px; border-radius: 50%;
{% if bs == 'OPERATIONAL' %}background: #10b981;
{% elif bs == 'CLOSED_TEMPORARILY' %}background: #f59e0b;
{% else %}background: #ef4444;{% endif %}"></span>
{% if bs == 'OPERATIONAL' %}Firma czynna
{% elif bs == 'CLOSED_TEMPORARILY' %}Tymczasowo zamknięta
{% elif bs == 'CLOSED_PERMANENTLY' %}Zamknięta na stałe
{% else %}{{ bs|replace('_', ' ')|title }}{% endif %}
</span>
</div>
{% endif %}
{% if places_data.primary_type %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Typ:</span>
<span style="color: var(--text-primary); font-weight: 500;">{{ places_data.primary_type_display or places_data.primary_type|replace('_', ' ')|title }}</span>
</div>
{% endif %}
{% if places_data.google_types and places_data.google_types is iterable and places_data.google_types is not string %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Wszystkie kategorie:</span>
<span style="color: var(--text-secondary);">
{% for t in places_data.google_types[:8] %}
<span style="display: inline-block; padding: 1px 6px; margin: 1px; background: #eff6ff; color: #1d4ed8; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">{{ t|replace('_', ' ')|title }}</span>
{% endfor %}
</span>
</div>
{% endif %}
{% if places_data.editorial_summary %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Opis Google:</span>
<span style="color: var(--text-secondary);">{{ places_data.editorial_summary }}</span>
</div>
{% endif %}
{% if places_data.google_website %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Strona WWW:</span>
<a href="{{ places_data.google_website }}" target="_blank" rel="noopener" style="color: #2563eb; text-decoration: none;">{{ places_data.google_website[:60] }}{% if places_data.google_website|length > 60 %}...{% endif %}</a>
</div>
{% endif %}
{% if places_data.google_place_id %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Place ID:</span>
<span style="color: var(--text-secondary); font-family: monospace; font-size: 11px;">{{ places_data.google_place_id }}</span>
</div>
{% endif %}
{% if places_data.price_level is not none and places_data.price_level %}
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
<span style="color: var(--text-tertiary);">Poziom cen:</span>
<span style="color: var(--text-primary);">
{% if places_data.price_level == 'PRICE_LEVEL_FREE' %}Bezpłatne
{% elif places_data.price_level == 'PRICE_LEVEL_INEXPENSIVE' %}$ Niedrogi
{% elif places_data.price_level == 'PRICE_LEVEL_MODERATE' %}$$ Umiarkowany
{% elif places_data.price_level == 'PRICE_LEVEL_EXPENSIVE' %}$$$ Drogi
{% elif places_data.price_level == 'PRICE_LEVEL_VERY_EXPENSIVE' %}$$$$ Bardzo drogi
{% else %}{{ places_data.price_level|replace('PRICE_LEVEL_', '')|replace('_', ' ')|title }}
{% endif %}
</span>
</div>
{% endif %}
{# Google rating and reviews summary #}
{% if places_data.google_rating is not none %}
<div style="margin-top: var(--spacing-sm); padding-top: var(--spacing-sm); border-top: 1px solid var(--border); display: flex; gap: var(--spacing-lg); flex-wrap: wrap; font-size: var(--font-size-sm);">
<div>
<span style="color: var(--text-tertiary);">Ocena:</span>
<span style="font-weight: 700; color: #f59e0b;">
{% for i in range(5) %}
<span style="color: {{ '#f59e0b' if i < (places_data.google_rating|round) else '#d1d5db' }};">&#9733;</span>
{% endfor %}
{{ places_data.google_rating }}/5
</span>
</div>
{% if places_data.google_reviews_count %}
<div>
<span style="color: var(--text-tertiary);">Opinii:</span>
<span style="font-weight: 600; color: var(--text-primary);">{{ places_data.google_reviews_count }}</span>
</div>
{% endif %}
{% if places_data.google_photos_count %}
<div>
<span style="color: var(--text-tertiary);">Zdjęć:</span>
<span style="font-weight: 600; color: var(--text-primary);">{{ places_data.google_photos_count }}</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{# Website Tracking Indicators #}
{% if places_data and (places_data.has_google_analytics is not none or places_data.has_google_tag_manager is not none or places_data.has_google_maps_embed is not none) %}
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Integracja ze stroną WWW
</h3>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
{% if places_data.has_google_analytics is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_analytics else '#fee2e2' }};">
<span style="color: {{ '#10b981' if places_data.has_google_analytics else '#ef4444' }};">{{ '✓' if places_data.has_google_analytics else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Google Analytics</span>
</div>
{% endif %}
{% if places_data.has_google_tag_manager is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_tag_manager else '#fee2e2' }};">
<span style="color: {{ '#10b981' if places_data.has_google_tag_manager else '#ef4444' }};">{{ '✓' if places_data.has_google_tag_manager else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Google Tag Manager</span>
</div>
{% endif %}
{% if places_data.has_google_maps_embed is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_maps_embed else '#f3f4f6' }};">
<span style="color: {{ '#10b981' if places_data.has_google_maps_embed else '#6b7280' }};">{{ '✓' if places_data.has_google_maps_embed else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Mapa Google na stronie</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
{# Google Attributes #}
{% if places_data and places_data.google_attributes and places_data.google_attributes is mapping %}
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
Atrybuty Google Business
</h3>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-xs);">
{% for key, value in places_data.google_attributes.items() %}
<span style="padding: 4px 10px; background: {{ '#dcfce7' if value else '#f3f4f6' }}; color: {{ '#10b981' if value else '#6b7280' }}; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">
{{ key|replace('_', ' ')|replace('.', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} &#10007;{% endif %}
</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if places_data and places_data.google_name %}
<!-- NAP Comparison -->
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
Porównanie NAP (Name, Address, Phone)
</h3>
<p style="font-size: 12px; color: var(--text-tertiary); margin: 0 0 var(--spacing-sm) 0;">Spójność danych NAP wpływa na lokalne SEO. Różnice mogą obniżać widoczność w Google.</p>
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="border-bottom: 2px solid var(--border);">
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 15%;">Pole</th>
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 35%;">Nasza baza</th>
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 35%;">Google</th>
<th style="text-align: center; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 15%;">Status</th>
</tr>
</thead>
<tbody>
{% set name_match = (company.name|lower|trim == places_data.google_name|lower|trim) if places_data.google_name else none %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; font-weight: 500;">Nazwa</td>
<td style="padding: 8px;">{{ company.name or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_name or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if name_match is none %}—
{% elif name_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
{% set our_addr = ((company.address_street or '') ~ ' ' ~ (company.address_city or ''))|trim %}
{% set addr_match = (our_addr|lower in (places_data.google_address|lower) or (places_data.google_address|lower) in our_addr|lower) if (places_data.google_address and our_addr) else none %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; font-weight: 500;">Adres</td>
<td style="padding: 8px;">{{ our_addr or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_address or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if addr_match is none %}—
{% elif addr_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
{% set phone_clean = (company.phone or '')|replace(' ','')|replace('-','')|replace('+48','') %}
{% set gphone_clean = (places_data.google_phone or '')|replace(' ','')|replace('-','')|replace('+48','') %}
{% set phone_match = (phone_clean == gphone_clean) if (phone_clean and gphone_clean) else none %}
<tr>
<td style="padding: 8px; font-weight: 500;">Telefon</td>
<td style="padding: 8px;">{{ company.phone or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_phone or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if phone_match is none %}—
{% elif phone_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
<!-- Fields Section -->
<div class="fields-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
Status pól wizytówki
</h2>
<div class="legend">
<div class="legend-item">
<div class="legend-dot complete"></div>
<span>Kompletne</span>
</div>
<div class="legend-item">
<div class="legend-dot partial"></div>
<span>Częściowe</span>
</div>
<div class="legend-item">
<div class="legend-dot missing"></div>
<span>Brakujące</span>
</div>
</div>
<div class="fields-grid">
{% set field_icons = {
'name': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>',
'address': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>',
'phone': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>',
'website': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>',
'hours': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>',
'categories': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>',
'photos': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>',
'description': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>',
'services': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>',
'reviews': '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>'
} %}
{% set field_names_pl = {
'name': 'Nazwa firmy',
'address': 'Adres',
'phone': 'Telefon',
'website': 'Strona WWW',
'hours': 'Godziny otwarcia',
'categories': 'Kategorie',
'photos': 'Zdjęcia',
'description': 'Opis',
'services': 'Usługi',
'reviews': 'Opinie'
} %}
{% set status_names = {
'complete': 'Kompletne',
'partial': 'Częściowe',
'missing': 'Brakuje'
} %}
{% for field_name, field_data in audit.fields_status.items() %}
<div class="field-card {{ field_data.status }}">
<div class="field-header">
<span class="field-name">
<svg class="field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{{ field_icons.get(field_name, '')|safe }}
</svg>
{{ field_names_pl.get(field_name, field_name) }}
</span>
<span class="field-status-badge {{ field_data.status }}">
{{ status_names.get(field_data.status, field_data.status) }}
</span>
</div>
{% if field_data.value %}
<div class="field-value">
{% if field_name == 'hours' and field_data.value is mapping and field_data.value.weekday_text %}
{# Translate English day names to Polish and show ALL 7 days #}
{% set day_pl = {'Monday': 'Pon', 'Tuesday': 'Wt', 'Wednesday': 'Sr', 'Thursday': 'Czw', 'Friday': 'Pt', 'Saturday': 'Sob', 'Sunday': 'Ndz'} %}
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px var(--spacing-sm); font-size: var(--font-size-xs);">
{% for day_hours in field_data.value.weekday_text %}
{% set parts = day_hours.split(': ', 1) %}
<span style="color: var(--text-tertiary); font-weight: 500;">{{ day_pl.get(parts[0], parts[0]) }}</span>
<span>{{ parts[1] if parts|length > 1 else day_hours }}</span>
{% endfor %}
</div>
{% elif field_name == 'hours' and field_data.value is string and 'weekday_text' in field_data.value %}
<span class="hours-compact">Godziny ustawione</span>
{% elif field_name == 'photos' %}
{{ field_data.value }} zdjęć w profilu
{% elif field_name == 'reviews' %}
{{ field_data.value }}
{% elif field_name == 'categories' %}
{% set cats = field_data.value.split(', ') if field_data.value is string else [field_data.value] %}
{% for cat in cats %}
<span style="display: inline-block; padding: 1px 6px; margin: 1px; background: #eff6ff; color: #1d4ed8; border-radius: var(--radius-sm); font-size: 11px;">{{ cat }}</span>
{% endfor %}
{% elif field_name == 'description' %}
{{ field_data.value[:120] }}{% if field_data.value|length > 120 %}...{% endif %}
{% else %}
{{ field_data.value }}
{% endif %}
</div>
{% endif %}
{% if field_name == 'reviews' %}
<div class="field-scoring-info reviews-scoring">
{% if field_data.score == field_data.max_score %}
<span class="scoring-message success">Doskonale! Maksymalna punktacja</span>
{% elif field_data.score == 0 %}
<span class="scoring-message hint">Popros zadowolonych klientow o opinie w Google</span>
{% else %}
<span class="scoring-message progress">Dobry poczatek! Zbierz wiecej opinii — cel to minimum 5</span>
{% endif %}
</div>
{% endif %}
<div class="field-score">
<div class="field-score-bar">
<div class="field-score-fill {{ field_data.status }}" style="width: {{ (field_data.score / field_data.max_score * 100) if field_data.max_score else 0 }}%;"></div>
</div>
<span class="field-score-text">{{ field_data.score|round(1) }}/{{ field_data.max_score }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% if audit.review_count and audit.review_count > 0 %}
<!-- Reviews Analysis Section -->
<div class="fields-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
Analiza opinii
</h2>
<div class="fields-grid">
<!-- Review Response Rate -->
{% if audit.review_response_rate is not none %}
<div class="field-card {{ 'complete' if audit.review_response_rate >= 80 else ('partial' if audit.review_response_rate >= 40 else 'missing') }}">
<div class="field-header">
<span class="field-name">Odpowiedzi na opinie</span>
<span class="field-status-badge {{ 'complete' if audit.review_response_rate >= 80 else ('partial' if audit.review_response_rate >= 40 else 'missing') }}">
{{ '%.0f'|format(audit.review_response_rate) }}%
</span>
</div>
<div class="field-value">
{{ audit.reviews_with_response or 0 }} z {{ (audit.reviews_with_response or 0) + (audit.reviews_without_response or 0) }} opinii z odpowiedzią
</div>
<div class="field-score">
<div class="field-score-bar">
<div class="field-score-fill {{ 'complete' if audit.review_response_rate >= 80 else ('partial' if audit.review_response_rate >= 40 else 'missing') }}" style="width: {{ audit.review_response_rate }}%;"></div>
</div>
</div>
</div>
{% endif %}
<!-- Review Sentiment -->
{% if audit.review_sentiment %}
{% set sentiment = audit.review_sentiment %}
<div class="field-card complete">
<div class="field-header">
<span class="field-name">Sentyment opinii</span>
</div>
<div class="field-value">
<div style="display: flex; gap: var(--spacing-md); margin-top: var(--spacing-xs);">
<span style="color: #10b981;">{{ sentiment.get('positive', 0) }} pozytywnych</span>
<span style="color: #f59e0b;">{{ sentiment.get('neutral', 0) }} neutralnych</span>
<span style="color: #ef4444;">{{ sentiment.get('negative', 0) }} negatywnych</span>
</div>
</div>
</div>
{% endif %}
<!-- Recent Reviews -->
{% if audit.reviews_30d is not none %}
<div class="field-card {{ 'complete' if audit.reviews_30d >= 3 else ('partial' if audit.reviews_30d >= 1 else 'missing') }}">
<div class="field-header">
<span class="field-name">Nowe opinie (30 dni)</span>
<span class="field-status-badge {{ 'complete' if audit.reviews_30d >= 3 else ('partial' if audit.reviews_30d >= 1 else 'missing') }}">
{{ audit.reviews_30d }}
</span>
</div>
</div>
{% endif %}
<!-- Review Keywords -->
{% if audit.review_keywords %}
<div class="field-card complete">
<div class="field-header">
<span class="field-name">Słowa kluczowe z opinii</span>
</div>
<div class="field-value">
{% for keyword in audit.review_keywords[:8] %}
<span style="display: inline-block; padding: 2px 8px; margin: 2px; background: #dbeafe; color: #1e40af; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">{{ keyword }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% if recent_reviews and recent_reviews|length > 0 %}
<!-- Recent Reviews -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
</svg>
Ostatnie recenzje
</h2>
<div style="background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden; margin-bottom: var(--spacing-md);">
{% for review in recent_reviews %}
<div style="padding: var(--spacing-md); border-bottom: 1px solid var(--border); {% if loop.last %}border-bottom: none;{% endif %}">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<strong style="font-size: var(--font-size-sm);">{{ review.author_name or 'Anonim' }}</strong>
<div style="display: flex; gap: 2px;">
{% for i in range(5) %}
<span style="color: {{ '#f59e0b' if i < review.rating else '#d1d5db' }}; font-size: 14px;">&#9733;</span>
{% endfor %}
</div>
{% if review.sentiment %}
<span style="font-size: var(--font-size-xs); padding: 1px 6px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if review.sentiment == 'positive' else ('#fef3c7' if review.sentiment == 'neutral' else '#fee2e2') }}; color: {{ '#10b981' if review.sentiment == 'positive' else ('#f59e0b' if review.sentiment == 'neutral' else '#ef4444') }};">{{ review.sentiment }}</span>
{% endif %}
</div>
{% if review.publish_time %}
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">{{ review.publish_time.strftime('%d.%m.%Y') }}</span>
{% endif %}
</div>
{% if review.text %}
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 0 0 var(--spacing-xs) 0; line-height: 1.5;">{{ review.text[:300] }}{% if review.text|length > 300 %}...{% endif %}</p>
{% endif %}
{% if review.has_owner_response and review.owner_response_text %}
<div style="margin-top: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-tertiary); border-left: 3px solid var(--primary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px;">
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-primary);">Odpowiedz wlasciciela:</span>
{% if review.owner_response_time %}
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">{{ review.owner_response_time.strftime('%d.%m.%Y') }}</span>
{% endif %}
</div>
<p style="font-size: var(--font-size-xs); color: var(--text-secondary); margin: 0; line-height: 1.4;">{{ review.owner_response_text[:200] }}{% if review.owner_response_text|length > 200 %}...{% endif %}</p>
</div>
{% endif %}
{% if review.keywords %}
<div style="margin-top: var(--spacing-xs);">
{% for kw in review.keywords[:5] %}
<span style="display: inline-block; padding: 1px 6px; margin: 1px; background: #eff6ff; color: #1d4ed8; border-radius: var(--radius-sm); font-size: 10px;">{{ kw }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{% if audit.description_keywords or audit.avg_review_response_days is not none %}
<!-- Keyword & Response Time Analysis -->
<div class="fields-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Analiza dodatkowa
</h2>
<div class="fields-grid">
{% if audit.avg_review_response_days is not none %}
{% set resp_days = audit.avg_review_response_days %}
<div class="field-card {{ 'complete' if resp_days <= 2 else ('partial' if resp_days <= 7 else 'missing') }}">
<div class="field-header">
<span class="field-name">
<svg class="field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Średni czas odpowiedzi
</span>
<span class="field-status-badge {{ 'complete' if resp_days <= 2 else ('partial' if resp_days <= 7 else 'missing') }}">
{{ '%.1f'|format(resp_days) }} dni
</span>
</div>
<div class="field-value">
{% if resp_days <= 2 %}
Doskonały czas reakcji na opinie klientów
{% elif resp_days <= 7 %}
Dobry czas reakcji — postaraj się odpowiadać w ciągu 1-2 dni
{% else %}
Długi czas odpowiedzi — klienci oczekują szybszej reakcji
{% endif %}
</div>
</div>
{% endif %}
{% if audit.description_keywords %}
<div class="field-card complete">
<div class="field-header">
<span class="field-name">
<svg class="field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
Słowa kluczowe w opisie
</span>
</div>
<div class="field-value">
{% for keyword in audit.description_keywords[:10] %}
<span style="display: inline-block; padding: 2px 8px; margin: 2px; background: #e0e7ff; color: #3730a3; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">{{ keyword }}</span>
{% endfor %}
</div>
{% if audit.keyword_density_score is not none %}
<div style="margin-top: var(--spacing-sm);">
<div style="display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 4px;">
<span>Gęstość słów kluczowych</span>
<span>{{ audit.keyword_density_score }}/10</span>
</div>
<div style="height: 6px; background: var(--border); border-radius: 3px; overflow: hidden;">
<div style="height: 100%; width: {{ audit.keyword_density_score * 10 }}%; border-radius: 3px; background: {% if audit.keyword_density_score >= 7 %}#10b981{% elif audit.keyword_density_score >= 4 %}#f59e0b{% else %}#ef4444{% endif %};"></div>
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if audit.nap_consistent is not none %}
<!-- NAP Consistency Section -->
<div class="fields-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Spójność NAP (Nazwa / Adres / Telefon)
</h2>
{% if audit.nap_consistent %}
<div style="padding: var(--spacing-md); background: #dcfce7; border-radius: var(--radius); border-left: 4px solid #10b981; color: #166534;">
Dane NAP na wizytówce Google są spójne z danymi na stronie WWW firmy.
</div>
{% else %}
<div style="padding: var(--spacing-md); background: #fef3c7; border-radius: var(--radius); border-left: 4px solid #f59e0b; color: #92400e; margin-bottom: var(--spacing-md);">
Wykryto różnice między wizytówką Google a stroną WWW firmy.
</div>
{% if audit.nap_issues %}
<div class="fields-grid">
{% for issue in audit.nap_issues %}
<div class="field-card missing">
<div class="field-header">
<span class="field-name">{{ issue.field|capitalize }}</span>
<span class="field-status-badge missing">Rozbieżność</span>
</div>
<div class="field-value">
<div><strong>Google:</strong> {{ issue.gbp or 'Brak' }}</div>
<div><strong>Strona WWW:</strong> {{ issue.website or 'Brak' }}</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
{% if audit.has_posts is not none or audit.attributes %}
<!-- Activity & Attributes -->
<div class="fields-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
Aktywność i atrybuty
</h2>
<div class="fields-grid">
{% if audit.has_posts is not none %}
<div class="field-card {{ 'complete' if audit.has_posts else 'missing' }}">
<div class="field-header">
<span class="field-name">Posty w Google</span>
<span class="field-status-badge {{ 'complete' if audit.has_posts else 'missing' }}">{{ 'Aktywne' if audit.has_posts else 'Brak' }}</span>
</div>
{% if audit.posts_count_30d %}
<div class="field-value">{{ audit.posts_count_30d }} postów w ostatnich 30 dniach</div>
{% endif %}
</div>
{% endif %}
{% if audit.has_qa is not none %}
<div class="field-card {{ 'complete' if audit.has_qa else 'partial' }}">
<div class="field-header">
<span class="field-name">Pytania i odpowiedzi</span>
<span class="field-status-badge {{ 'complete' if audit.has_qa else 'partial' }}">{{ audit.qa_count or 0 }}</span>
</div>
</div>
{% endif %}
{% if audit.has_products is not none %}
<div class="field-card {{ 'complete' if audit.has_products else 'partial' }}">
<div class="field-header">
<span class="field-name">Produkty / Menu</span>
<span class="field-status-badge {{ 'complete' if audit.has_products else 'partial' }}">{{ 'Dodane' if audit.has_products else 'Brak' }}</span>
</div>
</div>
{% endif %}
{% if audit.has_special_hours is not none %}
<div class="field-card {{ 'complete' if audit.has_special_hours else 'partial' }}">
<div class="field-header">
<span class="field-name">Godziny specjalne</span>
<span class="field-status-badge {{ 'complete' if audit.has_special_hours else 'partial' }}">{{ 'Ustawione' if audit.has_special_hours else 'Brak' }}</span>
</div>
{% if audit.special_hours %}
<div style="margin-top: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--text-secondary);">
{% for entry in audit.special_hours %}
<div>{{ entry.get('date', '') }}: {% if entry.get('closed') %}Zamknięte{% else %}{{ entry.get('open', '') }} - {{ entry.get('close', '') }}{% endif %}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% if audit.logo_present is not none or audit.cover_photo_present is not none %}
<div style="display: flex; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); margin-top: var(--spacing-lg);">
{% if audit.logo_present is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if audit.logo_present else '#fee2e2' }};">
<span style="color: {{ '#10b981' if audit.logo_present else '#ef4444' }};">{{ '✓' if audit.logo_present else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Logo</span>
</div>
{% endif %}
{% if audit.cover_photo_present is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if audit.cover_photo_present else '#fee2e2' }};">
<span style="color: {{ '#10b981' if audit.cover_photo_present else '#ef4444' }};">{{ '✓' if audit.cover_photo_present else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Zdjęcie w tle</span>
</div>
{% endif %}
</div>
{% endif %}
{% if audit.photo_categories %}
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm);">Kategorie zdjęć</h3>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
{% for category, count in audit.photo_categories.items() %}
<span style="padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary);">
{{ category|capitalize }}: {{ count }}
</span>
{% endfor %}
</div>
{% endif %}
{% if audit.attributes %}
<!-- Business Attributes -->
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm); display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
Atrybuty Google Business
</h3>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-xs);">
{% for key, value in audit.attributes.items() %}
<span style="padding: 4px 10px; background: {{ '#dcfce7' if value else '#f3f4f6' }}; color: {{ '#10b981' if value else '#6b7280' }}; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">
{{ key|replace('_', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} &#10007;{% endif %}
</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<!-- GBP Performance Section (from Performance API) -->
{% if places_data and places_data.gbp_impressions_maps is not none %}
<div class="card" style="margin-bottom: var(--spacing-md); padding: var(--spacing-xl);">
<h2 class="section-title" style="margin-bottom: var(--spacing-lg);">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Statystyki widocznosci ({{ places_data.gbp_performance_period_days or 30 }} dni)
</h2>
<!-- Impressions -->
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-bottom: var(--spacing-sm); color: var(--text-secondary);">Wyswietlenia profilu</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--spacing-md); margin-bottom: var(--spacing-lg);">
<div style="padding: var(--spacing-md); background: #eff6ff; border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: #1d4ed8;">{{ '{:,}'.format(places_data.gbp_impressions_maps or 0) }}</div>
<div style="font-size: var(--font-size-xs); color: #3b82f6; margin-top: 4px;">Google Maps</div>
</div>
<div style="padding: var(--spacing-md); background: #fef3c7; border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: #92400e;">{{ '{:,}'.format(places_data.gbp_impressions_search or 0) }}</div>
<div style="font-size: var(--font-size-xs); color: #d97706; margin-top: 4px;">Wyszukiwarka Google</div>
</div>
<div style="padding: var(--spacing-md); background: #f0fdf4; border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: #166534;">{{ '{:,}'.format((places_data.gbp_impressions_maps or 0) + (places_data.gbp_impressions_search or 0)) }}</div>
<div style="font-size: var(--font-size-xs); color: #16a34a; margin-top: 4px;">Lacznie</div>
</div>
</div>
<!-- Actions -->
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-bottom: var(--spacing-sm); color: var(--text-secondary);">Akcje klientow</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--spacing-md); margin-bottom: var(--spacing-lg);">
<div style="padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary);">{{ places_data.gbp_call_clicks or 0 }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Klikniecia telefon</div>
</div>
<div style="padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary);">{{ places_data.gbp_website_clicks or 0 }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Klikniecia strona</div>
</div>
<div style="padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary);">{{ places_data.gbp_direction_requests or 0 }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Prosby o trase</div>
</div>
{% if places_data.gbp_conversations %}
<div style="padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary);">{{ places_data.gbp_conversations }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Rozmowy</div>
</div>
{% endif %}
</div>
<!-- Search Keywords -->
{% if places_data.gbp_search_keywords %}
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-bottom: var(--spacing-sm); color: var(--text-secondary);">Frazy wyszukiwania</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="border-bottom: 2px solid var(--border-color);">
<th style="text-align: left; padding: 8px; color: var(--text-secondary); font-weight: 500;">#</th>
<th style="text-align: left; padding: 8px; color: var(--text-secondary); font-weight: 500;">Fraza</th>
<th style="text-align: right; padding: 8px; color: var(--text-secondary); font-weight: 500;">Wyswietlenia</th>
</tr>
</thead>
<tbody>
{% for kw in places_data.gbp_search_keywords[:10] %}
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 8px; color: var(--text-secondary);">{{ loop.index }}</td>
<td style="padding: 8px; font-weight: 500;">{{ kw.keyword }}</td>
<td style="padding: 8px; text-align: right; color: var(--text-secondary);">{{ '{:,}'.format(kw.impressions) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endif %}
<!-- Google Posts Section -->
{% if places_data and places_data.google_posts_data %}
<div class="card" style="margin-bottom: var(--spacing-md); padding: var(--spacing-xl);">
<h2 class="section-title" style="margin-bottom: var(--spacing-lg);">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
Google Posts ({{ places_data.google_posts_count or places_data.google_posts_data|length }})
</h2>
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
{% for post in places_data.google_posts_data[:5] %}
<div style="padding: var(--spacing-md); border: 1px solid var(--border-color); border-radius: var(--radius-md);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span style="font-size: var(--font-size-xs); padding: 2px 8px; background: #eff6ff; color: #1d4ed8; border-radius: var(--radius-sm); font-weight: 500;">
{{ post.topicType|default(post.get('searchUrl', 'POST')|default('Post'))|replace('_', ' ')|title }}
</span>
{% if post.createTime or post.get('createTime') %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">
{{ (post.createTime or post.get('createTime', ''))[:10] }}
</span>
{% endif %}
</div>
{% set summary = post.get('summary', post.get('text', '')) %}
{% if summary %}
<p style="margin: 0; font-size: var(--font-size-sm); color: var(--text-secondary); line-height: 1.5;">
{{ summary[:200] }}{% if summary|length > 200 %}...{% endif %}
</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Owner Response Rate (enhanced) -->
{% if places_data and places_data.google_owner_responses_count is not none %}
<div class="card" style="margin-bottom: var(--spacing-md); padding: var(--spacing-xl);">
<h2 class="section-title" style="margin-bottom: var(--spacing-lg);">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
</svg>
Odpowiedzi na opinie
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--spacing-md);">
<div style="padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary);">{{ places_data.google_owner_responses_count }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Odpowiedzi wlasciciela</div>
</div>
{% if places_data.google_review_response_rate is not none %}
<div style="padding: var(--spacing-md); background: {{ '#dcfce7' if places_data.google_review_response_rate >= 80 else '#fef3c7' if places_data.google_review_response_rate >= 50 else '#fee2e2' }}; border-radius: var(--radius-md); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: {{ '#166534' if places_data.google_review_response_rate >= 80 else '#92400e' if places_data.google_review_response_rate >= 50 else '#991b1b' }};">{{ places_data.google_review_response_rate }}%</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 4px;">Wskaznik odpowiedzi</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Recommendations Section -->
{% if audit.recommendations %}
<div class="recommendations-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Rekomendacje ({{ audit.recommendations|length }})
</h2>
<div class="recommendation-list">
{% for rec in audit.recommendations %}
<div class="recommendation-item">
<div class="recommendation-priority {{ rec.priority }}">
{% if rec.priority == 'high' %}
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
{% elif rec.priority == 'medium' %}
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{% else %}
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{% endif %}
</div>
<div class="recommendation-content">
<div class="recommendation-field">{{ field_names_pl.get(rec.field, rec.field) }}</div>
<div class="recommendation-text">{{ rec.recommendation }}</div>
</div>
<div class="recommendation-impact">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
+{{ rec.impact }} pkt
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# Smart Recommendations (from backend analysis) #}
{% if gbp_recommendations and gbp_recommendations|length > 0 %}
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Automatyczne zalecenia
</h2>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for rec in gbp_recommendations %}
<div style="padding: var(--spacing-md); border-radius: var(--radius); display: flex; align-items: flex-start; gap: var(--spacing-sm);
{% if rec.severity == 'critical' %}background: #fef2f2; border-left: 4px solid #ef4444;
{% elif rec.severity == 'warning' %}background: #fffbeb; border-left: 4px solid #f59e0b;
{% elif rec.severity == 'success' %}background: #f0fdf4; border-left: 4px solid #10b981;
{% else %}background: #eff6ff; border-left: 4px solid #3b82f6;{% endif %}">
<span style="flex-shrink: 0; font-size: 18px;">
{% if rec.severity == 'critical' %}&#9888;
{% elif rec.severity == 'warning' %}&#9888;
{% elif rec.severity == 'success' %}&#10004;
{% else %}&#8505;{% endif %}
</span>
<div>
<span style="font-size: var(--font-size-xs); font-weight: 600; text-transform: uppercase; padding: 1px 6px; border-radius: var(--radius-sm); margin-bottom: 4px; display: inline-block;
{% if rec.severity == 'critical' %}background: #fee2e2; color: #991b1b;
{% elif rec.severity == 'warning' %}background: #fef3c7; color: #92400e;
{% elif rec.severity == 'success' %}background: #dcfce7; color: #166534;
{% else %}background: #dbeafe; color: #1e40af;{% endif %}">
{% if rec.severity == 'critical' %}Krytyczne
{% elif rec.severity == 'warning' %}Zalecenie
{% elif rec.severity == 'success' %}OK
{% else %}Informacja{% endif %}
</span>
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 4px 0 0 0; line-height: 1.5;">{{ rec.text }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# Benchmarks — comparison with other members #}
{% if gbp_benchmarks and gbp_benchmarks.count is defined and gbp_benchmarks.count > 1 %}
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Porownanie z innymi firmami Norda Biznes ({{ gbp_benchmarks.count }})
</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="border-bottom: 2px solid var(--border);">
<th style="text-align: left; padding: 10px; color: var(--text-tertiary); font-weight: 500;">Metryka</th>
<th style="text-align: center; padding: 10px; color: var(--text-primary); font-weight: 600;">{{ company.name[:20] }}{% if company.name|length > 20 %}...{% endif %}</th>
<th style="text-align: center; padding: 10px; color: var(--text-tertiary); font-weight: 500;">Srednia Norda Biznes</th>
<th style="text-align: center; padding: 10px; color: var(--text-tertiary); font-weight: 500;">vs Srednia</th>
</tr>
</thead>
<tbody>
{# Rating #}
{% if places_data.google_rating is not none %}
{% set diff_rating = places_data.google_rating - gbp_benchmarks.avg_rating %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 10px; font-weight: 500;">Ocena Google</td>
<td style="padding: 10px; text-align: center; font-weight: 700;">
<span style="color: #f59e0b;">&#9733;</span> {{ places_data.google_rating }}
</td>
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_rating }}</td>
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_rating >= 0 else '#ef4444' }};">
{{ '▲' if diff_rating > 0 else '▼' if diff_rating < 0 else '' }} {{ '%+.1f'|format(diff_rating) }}
</td>
</tr>
{% endif %}
{# Reviews count #}
{% if places_data.google_reviews_count is not none %}
{% set diff_reviews = (places_data.google_reviews_count or 0) - gbp_benchmarks.avg_reviews %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 10px; font-weight: 500;">Liczba opinii</td>
<td style="padding: 10px; text-align: center; font-weight: 700;">{{ places_data.google_reviews_count }}</td>
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_reviews }}</td>
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_reviews >= 0 else '#ef4444' }};">
{{ '▲' if diff_reviews > 0 else '▼' if diff_reviews < 0 else '' }} {{ '%+d'|format(diff_reviews|int) }}
</td>
</tr>
{% endif %}
{# Photos count #}
{% if places_data.google_photos_count is not none %}
{% set diff_photos = (places_data.google_photos_count or 0) - gbp_benchmarks.avg_photos %}
<tr>
<td style="padding: 10px; font-weight: 500;">Liczba zdjec</td>
<td style="padding: 10px; text-align: center; font-weight: 700;">{{ places_data.google_photos_count }}</td>
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_photos }}</td>
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_photos >= 0 else '#ef4444' }};">
{{ '▲' if diff_photos > 0 else '▼' if diff_photos < 0 else '' }} {{ '%+d'|format(diff_photos|int) }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# Google Reviews from Places API — data is dict with 'reviews' key #}
{% if places_data and places_data.google_reviews_data is mapping and places_data.google_reviews_data.get('reviews') %}
{% set api_reviews = places_data.google_reviews_data.reviews %}
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
</svg>
Opinie z Google Places API ({{ places_data.google_reviews_data.get('total_from_api', api_reviews|length) }} z {{ places_data.google_reviews_data.get('total_reported', '?') }})
</h2>
{# Rating distribution bar #}
{% if places_data.google_reviews_data.get('rating_distribution') %}
{% set rd = places_data.google_reviews_data.rating_distribution %}
{# JSONB stores keys as strings, so look up both int and string keys #}
{% set rd_total = rd.values()|sum %}
{% if rd_total > 0 %}
<div style="margin-bottom: var(--spacing-md);">
{% for star in [5, 4, 3, 2, 1] %}
{% set star_count = rd.get(star, rd.get(star|string, 0)) %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: 3px; font-size: var(--font-size-xs);">
<span style="width: 16px; text-align: right; color: var(--text-tertiary);">{{ star }}★</span>
<div style="flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: {{ (star_count / rd_total * 100)|round }}%; background: #f59e0b; border-radius: 4px;"></div>
</div>
<span style="width: 24px; text-align: right; color: var(--text-secondary);">{{ star_count }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{# Response rate from Places API data #}
{% if places_data.google_reviews_data.get('response_rate') is not none %}
<div style="margin-bottom: var(--spacing-md); font-size: var(--font-size-sm); color: var(--text-secondary);">
Odpowiedzi na opinie: {{ places_data.google_reviews_data.with_response|default(0) }} z {{ (places_data.google_reviews_data.with_response|default(0)) + (places_data.google_reviews_data.without_response|default(0)) }}
({{ '%.0f'|format(places_data.google_reviews_data.response_rate) }}%)
</div>
{% endif %}
{# Individual reviews #}
{% for review in api_reviews[:5] %}
<div style="padding: var(--spacing-md); border-bottom: 1px solid var(--border); {% if loop.last %}border-bottom: none;{% endif %}">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<strong style="font-size: var(--font-size-sm);">{{ review.author|default('Anonim') }}</strong>
<div style="display: flex; gap: 2px;">
{% for i in range(5) %}
<span style="color: {{ '#f59e0b' if i < (review.rating|default(0))|int else '#d1d5db' }}; font-size: 14px;"></span>
{% endfor %}
</div>
</div>
{% if review.relative_time %}
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">{{ review.relative_time }}</span>
{% endif %}
</div>
{% if review.text %}
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 0; line-height: 1.5;">{{ review.text[:300] }}{% if review.text|length > 300 %}...{% endif %}</p>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{# Google Photos Metadata #}
{% if places_data and places_data.google_photos_metadata is mapping and places_data.google_photos_metadata.get('photos') %}
{% set photos_meta = places_data.google_photos_metadata %}
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
Zdjecia w Google ({{ photos_meta.get('total_count', photos_meta.photos|length) }})
</h2>
{% if photos_meta.get('has_owner_photos') is not none %}
<div style="margin-bottom: var(--spacing-sm);">
<span style="display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); font-weight: 600;
{% if photos_meta.has_owner_photos %}background: #dcfce7; color: #166534;{% else %}background: #fef3c7; color: #92400e;{% endif %}">
{% if photos_meta.has_owner_photos %}✓ Wlasciciel dodal zdjecia{% else %}✗ Brak zdjec od wlasciciela{% endif %}
</span>
</div>
{% endif %}
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="border-bottom: 2px solid var(--border);">
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500;">#</th>
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500;">Autor</th>
<th style="text-align: center; padding: 8px; color: var(--text-tertiary); font-weight: 500;">Wymiary</th>
</tr>
</thead>
<tbody>
{% for photo in photos_meta.photos[:10] %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; color: var(--text-secondary);">{{ loop.index }}</td>
<td style="padding: 8px;">{{ photo.get('attribution', '—') }}</td>
<td style="padding: 8px; text-align: center; font-family: monospace; font-size: 11px; color: var(--text-secondary);">
{% if photo.get('width') and photo.get('height') %}{{ photo.width }}×{{ photo.height }}{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Educational Info Section -->
<div class="info-section">
<div class="info-section-header">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<h3>Jak działa wizytówka Google?</h3>
</div>
<div class="info-cards">
<div class="info-card">
<div class="info-card-icon search">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<h4>Wyszukiwarka Google</h4>
<p>Gdy ktoś szuka Twojej firmy w Google, po prawej stronie wyników pojawia się <strong>Panel Wiedzy</strong> (Knowledge Panel).</p>
<ul>
<li>Nazwa i logo firmy</li>
<li>Adres i godziny otwarcia</li>
<li>Ocena i opinie</li>
<li>Przycisk "Zadzwoń" i "Trasa"</li>
</ul>
</div>
<div class="info-card">
<div class="info-card-icon maps">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<h4>Mapy Google</h4>
<p>W aplikacji Google Maps Twoja firma ma <strong>pełną wizytówkę</strong> z dodatkowymi funkcjami.</p>
<ul>
<li>Zdjęcia i wirtualny spacer</li>
<li>Wszystkie opinie klientów</li>
<li>Pytania i odpowiedzi (Q&A)</li>
<li>Posty i aktualności firmy</li>
</ul>
</div>
<div class="info-card">
<div class="info-card-icon manage">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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.065 2.572c1.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.572 1.065c-.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.065-2.572c-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"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<h4>Jak zarządzać?</h4>
<p>Wszystkie dane edytujesz w <strong>jednym miejscu</strong> — panelu Google Business Profile.</p>
<ul>
<li>Wejdz na <a href="https://business.google.com" target="_blank" rel="noopener">business.google.com</a></li>
<li>Zaloguj się kontem Google</li>
<li>Edytuj dane — zaktualizują się wszędzie</li>
<li>Odpowiadaj na opinie klientów</li>
</ul>
</div>
</div>
<div class="info-summary">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p>
<strong>Jedno źródło, wiele widoków.</strong>
Panel Wiedzy w wyszukiwarce i wizytówka w Mapach Google to <strong>te same dane</strong> wyświetlane w różnych miejscach.
Wystarczy, że zarządzasz profilem w <a href="https://business.google.com" target="_blank" rel="noopener">Google Business Profile</a>
zmiany automatycznie pojawią się wszędzie: w wyszukiwarce, Mapach, Asystencie Google i wynikach lokalnych.
</p>
</div>
</div>
{% with audit_type='gbp' %}
{% include 'partials/audit_ai_actions.html' %}
{% endwith %}
{# Audit Diagnostics #}
{% if audit.audit_errors or audit.audit_source or audit.audit_version %}
<div style="background: var(--bg-tertiary); padding: var(--spacing-md); border-radius: var(--radius-lg); margin-bottom: var(--spacing-md); font-size: var(--font-size-xs); color: var(--text-tertiary);">
<div style="display: flex; gap: var(--spacing-lg); flex-wrap: wrap; margin-bottom: {{ 'var(--spacing-sm)' if audit.audit_errors else '0' }};">
{% if audit.audit_source %}
<span>Zrodlo: <strong style="color: var(--text-secondary);">{{ audit.audit_source }}</strong></span>
{% endif %}
{% if audit.audit_version %}
<span>Wersja: <strong style="color: var(--text-secondary);">{{ audit.audit_version }}</strong></span>
{% endif %}
{% if audit.audit_date %}
<span>Data: <strong style="color: var(--text-secondary);">{{ audit.audit_date.strftime('%d.%m.%Y %H:%M') }}</strong></span>
{% endif %}
</div>
{% if audit.audit_errors %}
<div style="padding: var(--spacing-sm); background: #fef3c7; border-radius: var(--radius-sm); border-left: 3px solid #f59e0b; color: #78350f;">
<strong style="color: #92400e;">Uwagi:</strong> {{ audit.audit_errors }}
</div>
{% endif %}
</div>
{% endif %}
{% else %}
<!-- No Audit State -->
<div class="no-audit-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>
<h2>Brak danych audytu</h2>
<p>Nie przeprowadzono jeszcze audytu wizytówki Google dla tej firmy. Uruchom audyt, aby sprawdzić kompletność profilu.</p>
{% if can_audit %}
<button class="btn btn-primary" onclick="runAudit()">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Uruchom pierwszy audyt
</button>
{% endif %}
</div>
{% endif %}
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<div class="loading-header">
<h3>Audyt Google Business Profile</h3>
<p>Pobieram dane z Google i analizuję wizytówkę...</p>
</div>
<div class="loading-steps" id="loadingSteps">
<!-- Phase 1: Find Place -->
<div class="loading-step" id="step-find">
<div class="step-icon in_progress">
<div class="step-spinner"></div>
</div>
<span class="step-text in_progress">Szukam firmy w Google Maps...</span>
</div>
<!-- Phase 2: Fetch Details - individual data points from Google -->
<div class="loading-step step-detail" id="step-rating">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Ocena <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-reviews">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Liczba opinii <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-photos">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Zdjęcia <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-hours">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Godziny otwarcia <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-phone">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Numer telefonu <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-website">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Strona WWW <span class="source-tag google">Google</span></span>
</div>
<div class="loading-step step-detail" id="step-status">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Status firmy <span class="source-tag google">Google</span></span>
</div>
<!-- Phase 3: Save -->
<div class="loading-step" id="step-save">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Zapisuje dane w bazie</span>
</div>
<!-- Phase 4: Audit -->
<div class="loading-step" id="step-audit">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Analizuję kompletność profilu</span>
</div>
</div>
</div>
</div>
<!-- Info Modal -->
<div class="modal" id="infoModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-icon info" id="modalIcon">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="modal-title" id="modalTitle">Informacja</div>
</div>
<div class="modal-body" id="modalBody">
Treść informacji.
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">OK</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
const companySlug = '{{ company.slug }}';
// All step IDs in order
const allSteps = [
'step-find',
'step-rating', 'step-reviews', 'step-photos', 'step-hours',
'step-phone', 'step-website', 'step-status',
'step-save', 'step-audit'
];
// Detail step labels (defaults) with source tags
const detailLabels = {
'step-rating': 'Ocena <span class="source-tag google">Google</span>',
'step-reviews': 'Liczba opinii <span class="source-tag google">Google</span>',
'step-photos': 'Zdjęcia <span class="source-tag google">Google</span>',
'step-hours': 'Godziny otwarcia <span class="source-tag google">Google</span>',
'step-phone': 'Numer telefonu <span class="source-tag google">Google</span>',
'step-website': 'Strona WWW <span class="source-tag google">Google</span>',
'step-status': 'Status firmy <span class="source-tag google">Google</span>'
};
// SVG icons for different states
const icons = {
pending: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg>',
in_progress: '<div class="step-spinner"></div>',
complete: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
error: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
warning: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
skipped: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>',
missing: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/></svg>'
};
function resetSteps() {
// Reset all steps to default labels and pending state
allSteps.forEach((stepId, index) => {
const stepEl = document.getElementById(stepId);
if (stepEl) {
const iconEl = stepEl.querySelector('.step-icon');
const textEl = stepEl.querySelector('.step-text');
// Reset label to default (use innerHTML to preserve source tags)
if (detailLabels[stepId]) {
textEl.innerHTML = detailLabels[stepId];
}
if (index === 0) {
iconEl.className = 'step-icon in_progress';
iconEl.innerHTML = icons.in_progress;
textEl.className = 'step-text in_progress';
} else {
iconEl.className = 'step-icon pending';
iconEl.innerHTML = icons.pending;
textEl.className = 'step-text pending';
}
}
});
}
function updateStep(stepId, status, message) {
const stepEl = document.getElementById(stepId);
if (!stepEl) return;
const iconEl = stepEl.querySelector('.step-icon');
const textEl = stepEl.querySelector('.step-text');
iconEl.className = 'step-icon ' + status;
iconEl.innerHTML = icons[status] || icons.pending;
textEl.className = 'step-text ' + status;
if (message) {
textEl.textContent = message;
}
}
function showLoading() {
resetSteps();
document.getElementById('loadingOverlay').classList.add('active');
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.remove('active');
}
function showInfoModal(title, body, isSuccess) {
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalBody').textContent = body;
const icon = document.getElementById('modalIcon');
icon.className = 'modal-icon ' + (isSuccess ? 'success' : 'info');
document.getElementById('infoModal').classList.add('active');
}
function closeInfoModal() {
document.getElementById('infoModal').classList.remove('active');
}
document.getElementById('infoModal')?.addEventListener('click', (e) => {
if (e.target.id === 'infoModal') closeInfoModal();
});
// Update detail steps with fetched data values
async function updateDetailSteps(googleData) {
const delay = 150; // ms between each step animation
// Rating
updateStep('step-rating', 'in_progress', 'Pobieram ocenę...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_rating) {
updateStep('step-rating', 'complete', `Ocena: ${googleData.google_rating}/5`);
} else {
updateStep('step-rating', 'missing', 'Brak oceny');
}
// Reviews
updateStep('step-reviews', 'in_progress', 'Pobieram opinie...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_reviews_count) {
updateStep('step-reviews', 'complete', `Opinie: ${googleData.google_reviews_count}`);
} else {
updateStep('step-reviews', 'missing', 'Brak opinii');
}
// Photos
updateStep('step-photos', 'in_progress', 'Pobieram zdjęcia...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_photos_count) {
updateStep('step-photos', 'complete', `Zdjęcia: ${googleData.google_photos_count}`);
} else {
updateStep('step-photos', 'missing', 'Brak zdjęć');
}
// Opening hours
updateStep('step-hours', 'in_progress', 'Pobieram godziny otwarcia...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_opening_hours && googleData.google_opening_hours.weekday_text) {
updateStep('step-hours', 'complete', 'Godziny otwarcia: ustawione');
} else {
updateStep('step-hours', 'missing', 'Brak godzin otwarcia');
}
// Phone
updateStep('step-phone', 'in_progress', 'Pobieram telefon...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_phone) {
updateStep('step-phone', 'complete', `Telefon: ${googleData.google_phone}`);
} else {
updateStep('step-phone', 'missing', 'Brak telefonu');
}
// Website
updateStep('step-website', 'in_progress', 'Pobieram stronę WWW...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_website) {
// Truncate long URLs
const shortUrl = googleData.google_website.replace(/^https?:\/\//, '').slice(0, 30);
updateStep('step-website', 'complete', `WWW: ${shortUrl}...`);
} else {
updateStep('step-website', 'missing', 'Brak strony WWW');
}
// Business status
updateStep('step-status', 'in_progress', 'Pobieram status...');
await new Promise(r => setTimeout(r, delay));
if (googleData.google_business_status) {
const statusMap = {
'OPERATIONAL': 'Czynna',
'CLOSED_TEMPORARILY': 'Tymczasowo zamknięta',
'CLOSED_PERMANENTLY': 'Zamknięta na stałe'
};
const statusText = statusMap[googleData.google_business_status] || googleData.google_business_status;
updateStep('step-status', 'complete', `Status: ${statusText}`);
} else {
updateStep('step-status', 'missing', 'Brak statusu');
}
}
async function runAudit() {
const btn = document.getElementById('runAuditBtn');
if (btn) {
btn.disabled = true;
}
showLoading();
// Simulate step animation start
await new Promise(r => setTimeout(r, 300));
try {
const response = await fetch('/api/gbp/audit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ slug: companySlug, force_refresh: true })
});
const data = await response.json();
// Handle find_place step
if (data.google_fetch && data.google_fetch.steps) {
const findStep = data.google_fetch.steps.find(s => s.step === 'find_place');
if (findStep) {
updateStep('step-find', findStep.status, findStep.message);
}
}
// If we have Google data, animate the detail steps with actual values
if (data.google_fetch && data.google_fetch.data && data.google_fetch.success) {
await updateDetailSteps(data.google_fetch.data);
// Save step
const saveStep = data.google_fetch.steps.find(s => s.step === 'save_data');
if (saveStep) {
updateStep('step-save', saveStep.status, saveStep.message);
}
} else if (data.google_fetch && data.google_fetch.steps) {
// Mark detail steps as skipped if no Google data
const detailStepIds = ['step-rating', 'step-reviews', 'step-photos', 'step-hours', 'step-phone', 'step-website', 'step-status'];
for (const stepId of detailStepIds) {
updateStep(stepId, 'skipped', detailLabels[stepId] + ' (pominięty)');
}
}
// Update audit step
if (response.ok && data.success) {
updateStep('step-save', 'complete', 'Dane zapisane');
updateStep('step-audit', 'complete', `Analiza zakończona: ${data.audit?.total_score || 0}/100`);
// Wait 5 seconds so user can read the progress steps
await new Promise(r => setTimeout(r, 5000));
hideLoading();
showInfoModal('Audyt zakończony', 'Audyt wizytówki Google został zakończony pomyślnie. Strona zostanie odświeżona.', true);
setTimeout(() => location.reload(), 1500);
} else {
updateStep('step-audit', 'error', 'Błąd audytu');
// Wait 5 seconds so user can see what failed
await new Promise(r => setTimeout(r, 5000));
hideLoading();
showInfoModal('Błąd', data.error || 'Wystąpił nieznany błąd podczas audytu.', false);
if (btn) btn.disabled = false;
}
} catch (error) {
hideLoading();
showInfoModal('Błąd połączenia', 'Nie udało się połączyć z serwerem: ' + error.message, false);
if (btn) btn.disabled = false;
}
}
/* ============================================================
AI AUDIT ACTIONS
============================================================ */
const companyId = {{ company.id }};
const auditType = 'gbp';
function simpleMarkdown(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
.replace(/\n/g, '<br>');
}
async function runAIAnalysis(force) {
const prompt = document.getElementById('aiAnalyzePrompt');
const loading = document.getElementById('aiLoading');
const results = document.getElementById('aiResults');
const btn = document.getElementById('aiAnalyzeBtn');
if (btn) btn.disabled = true;
if (prompt) prompt.style.display = 'none';
if (results) results.style.display = 'none';
if (loading) loading.style.display = 'block';
let seconds = 0;
const timerEl = document.getElementById('aiTimer');
const timerInterval = setInterval(() => {
seconds++;
if (timerEl) timerEl.textContent = seconds + 's';
}, 1000);
try {
const response = await fetch('/api/audit/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
company_id: companyId,
audit_type: auditType,
force: !!force
})
});
const data = await response.json();
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (data.success) {
renderAIResults(data);
} else {
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Błąd analizy AI</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany błąd')}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Spróbuj ponownie</button>
</div>`;
results.style.display = 'block';
}
} catch (error) {
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Błąd połączenia</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Spróbuj ponownie</button>
</div>`;
results.style.display = 'block';
}
}
function renderAIResults(data) {
const results = document.getElementById('aiResults');
const summaryEl = document.getElementById('aiSummaryText');
const cacheInfo = document.getElementById('aiCacheInfo');
const actionsList = document.getElementById('aiActionsList');
summaryEl.textContent = data.summary || '';
if (data.cached && data.generated_at) {
const d = new Date(data.generated_at);
document.getElementById('aiCacheDate').textContent =
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
cacheInfo.style.display = 'block';
} else {
cacheInfo.style.display = 'none';
}
actionsList.innerHTML = '';
const actions = data.actions || [];
const priorityLabels = {critical: 'KRYTYCZNE', high: 'WYSOKI', medium: 'ŚREDNI', low: 'NISKI'};
actions.forEach((action, idx) => {
const impact = action.impact_score || 5;
const effort = action.effort_score || 5;
const card = document.createElement('div');
card.className = 'ai-action-card priority-' + (action.priority || 'medium');
card.id = 'ai-action-' + idx;
card.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-sm); flex-wrap: wrap; gap: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<span class="ai-priority-badge ${action.priority || 'medium'}">${priorityLabels[action.priority] || 'ŚREDNI'}</span>
<span class="ai-action-title" style="font-weight: 600; color: var(--text-primary);">${escapeHtml(action.title || '')}</span>
</div>
</div>
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm);">${escapeHtml(action.description || '')}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin-bottom: var(--spacing-sm);">
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wpływ: ${impact}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill impact" style="width: ${impact * 10}%;"></div></div>
</div>
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wysiłek: ${effort}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill effort" style="width: ${effort * 10}%;"></div></div>
</div>
</div>
<div class="ai-action-buttons">
<button class="btn btn-outline btn-sm" onclick="generateContent('${action.action_type}', ${idx})">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
Wygeneruj treść
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'implemented')" style="color: #10b981; border-color: #10b981;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Zrobione
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'dismissed')" style="color: var(--text-tertiary); border-color: var(--border);">Odrzuc</button>
</div>
<div id="ai-content-${idx}" style="display: none;"></div>
`;
actionsList.appendChild(card);
});
// Render comparison with previous analysis if available
if (typeof renderAIComparison === 'function') renderAIComparison(data);
results.style.display = 'block';
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
window._aiActions = actions;
}
async function generateContent(actionType, idx) {
const container = document.getElementById('ai-content-' + idx);
if (!container) return;
if (container.dataset.loaded === 'true') {
container.style.display = container.style.display === 'none' ? 'block' : 'none';
return;
}
container.innerHTML = '<div style="padding: var(--spacing-md); color: var(--text-secondary); font-size: var(--font-size-sm);">Generowanie treści...</div>';
container.style.display = 'block';
try {
const response = await fetch('/api/audit/generate-content', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken},
body: JSON.stringify({company_id: companyId, action_type: actionType, context: {}})
});
const data = await response.json();
if (data.success && data.content) {
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
if (isCode) {
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
} else {
container.innerHTML = `
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
</div>`;
}
container.dataset.loaded = 'true';
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
} else {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(data.error || 'Błąd generowania')}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
} catch (error) {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
}
function copyContent(btn) {
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
if (!code) return;
navigator.clipboard.writeText(code.textContent).then(() => {
const orig = btn.textContent;
btn.textContent = 'Skopiowano!';
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
function markAction(idx, status) {
const card = document.getElementById('ai-action-' + idx);
if (!card) return;
if (status === 'implemented') card.classList.add('implemented');
else if (status === 'dismissed') card.classList.add('dismissed');
const actions = window._aiActions || [];
if (actions[idx] && actions[idx].id) {
fetch('/api/audit/actions/' + actions[idx].id + '/status', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken},
body: JSON.stringify({status: status})
}).catch(() => {});
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
{% endblock %}