nordabiz/templates/admin/company_wizard.html
Maciej Pienczyn 5145c67d24
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
improve(wizard): auto-shorten name, PKD→category, clean WWW, UX polish
1. Auto-strip legal form from name (CONSTELLATION SP. Z O.O. → Constellation)
2. Auto-suggest category from main PKD code (62.02.Z → IT)
3. Clean empty 'https://' from WWW field
4. Rename button to 'Wyszukaj w internecie'
5. Auto-advance to step 4 after logo confirmation
6. Larger logo preview in summary step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:54:17 +02:00

1264 lines
56 KiB
HTML

{% extends "base.html" %}
{% block title %}Kreator Dodawania Firmy - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.wizard-container {
max-width: 860px;
margin: 0 auto;
}
.wizard-header {
margin-bottom: var(--spacing-2xl);
text-align: center;
}
.wizard-header h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin: 0 0 var(--spacing-md) 0;
}
.wizard-steps {
display: flex;
justify-content: center;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
}
.wizard-step {
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-size-sm);
cursor: default;
}
.wizard-step.active { color: var(--primary); font-weight: 600; }
.wizard-step.completed { color: var(--success); }
.step-number {
width: 32px; height: 32px;
border-radius: 50%;
background: var(--border);
display: flex; align-items: center; justify-content: center;
font-weight: 600; font-size: var(--font-size-sm);
}
.wizard-step.active .step-number { background: var(--primary); color: white; }
.wizard-step.completed .step-number { background: var(--success); color: white; }
.wizard-content {
background: var(--surface);
padding: var(--spacing-2xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.step-panel { display: none; }
.step-panel.active { display: block; }
.form-section { margin-bottom: var(--spacing-xl); }
.form-section h2 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin: 0 0 var(--spacing-lg) 0;
padding-bottom: var(--spacing-sm);
border-bottom: 2px solid var(--border);
}
.form-group { margin-bottom: var(--spacing-lg); }
.form-group label {
display: block; font-weight: 500;
margin-bottom: var(--spacing-xs); color: var(--text-primary);
}
.form-group label .required { color: var(--error); }
.form-control {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
transition: var(--transition);
background: var(--surface);
color: var(--text-primary);
}
.form-control:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1); }
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
.form-hint { font-size: var(--font-size-sm); color: var(--text-secondary); margin-top: var(--spacing-xs); }
.nip-input-row { display: flex; gap: var(--spacing-sm); }
.nip-input-row .form-control { flex: 1; }
.btn-wizard {
padding: var(--spacing-sm) var(--spacing-xl);
border: none; border-radius: var(--radius);
font-size: var(--font-size-base); font-weight: 500;
cursor: pointer; transition: var(--transition);
display: inline-flex; align-items: center; gap: var(--spacing-sm);
}
.btn-wizard:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover:not(:disabled) { opacity: 0.9; }
.btn-secondary { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); }
.btn-secondary:hover:not(:disabled) { background: var(--background); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover:not(:disabled) { opacity: 0.9; }
.btn-danger { background: var(--error); color: white; }
.btn-danger:hover:not(:disabled) { opacity: 0.9; }
.btn-link { background: none; border: none; color: var(--primary); cursor: pointer; padding: 0; font-size: var(--font-size-sm); text-decoration: underline; }
.wizard-actions {
display: flex; justify-content: space-between; align-items: center;
margin-top: var(--spacing-xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
}
/* Status indicators */
.status-badge {
display: inline-flex; align-items: center; gap: var(--spacing-xs);
padding: 2px var(--spacing-sm);
border-radius: var(--radius-full);
font-size: var(--font-size-xs); font-weight: 600;
}
.status-badge.krs { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.status-badge.ceidg { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.status-badge.manual { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
.lookup-status {
margin-top: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md);
background: var(--background); border-radius: var(--radius);
font-size: var(--font-size-sm); color: var(--text-secondary);
}
.lookup-status.loading { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.lookup-status.success { background: rgba(16, 185, 129, 0.05); color: var(--success); }
.lookup-status.error { background: rgba(239, 68, 68, 0.05); color: var(--error); }
.registry-preview {
background: var(--background); border: 1px solid var(--border);
border-radius: var(--radius); padding: var(--spacing-lg);
margin-top: var(--spacing-md);
}
.registry-preview.success { border-color: var(--success); background: rgba(var(--success-rgb), 0.03); }
.registry-preview h4 {
margin: 0 0 var(--spacing-sm) 0; color: var(--success);
display: flex; align-items: center; gap: var(--spacing-sm);
}
.registry-data { display: grid; gap: var(--spacing-xs); }
.registry-data-row { display: flex; gap: var(--spacing-sm); }
.registry-data-label { font-weight: 500; color: var(--text-secondary); min-width: 120px; }
/* Logo gallery */
.logo-gallery {
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--spacing-md); margin-top: var(--spacing-md);
}
.logo-candidate {
position: relative; padding: var(--spacing-sm); border: 2px solid var(--border);
border-radius: var(--radius); text-align: center; cursor: pointer;
transition: var(--transition); background: var(--surface);
}
.logo-candidate:hover { border-color: var(--primary); }
.logo-candidate.selected { border-color: var(--success); background: rgba(var(--success-rgb), 0.03); }
.logo-candidate img { max-width: 100%; max-height: 80px; object-fit: contain; }
.logo-candidate .label { font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: var(--spacing-xs); }
.logo-candidate.recommended::after {
content: 'Rekomendowane'; position: absolute; top: -8px; right: -8px;
background: var(--primary); color: white; font-size: 10px;
padding: 1px 6px; border-radius: var(--radius-full);
}
/* Audit cards */
.audit-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--spacing-md); }
.audit-card {
padding: var(--spacing-lg); border: 1px solid var(--border);
border-radius: var(--radius); background: var(--surface);
}
.audit-card-header { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); }
.audit-card-header h3 { margin: 0; font-size: var(--font-size-base); }
.audit-icon { font-size: var(--font-size-xl); }
.audit-status {
display: inline-flex; align-items: center; gap: var(--spacing-xs);
font-size: var(--font-size-sm); padding: 2px var(--spacing-sm);
border-radius: var(--radius-full);
}
.audit-status.pending { background: var(--background); color: var(--text-secondary); }
.audit-status.running { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.audit-status.completed { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.audit-status.error { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
.audit-status.skipped { background: var(--background); color: var(--text-secondary); }
.audit-score { font-size: var(--font-size-2xl); font-weight: 700; margin-top: var(--spacing-sm); }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
/* Summary cards */
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--spacing-md); }
.summary-card {
padding: var(--spacing-lg); border: 1px solid var(--border);
border-radius: var(--radius); background: var(--surface);
}
.summary-card h3 {
margin: 0 0 var(--spacing-md) 0; font-size: var(--font-size-base);
color: var(--text-primary); padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border);
}
.summary-row { display: flex; justify-content: space-between; padding: var(--spacing-xs) 0; }
.summary-label { color: var(--text-secondary); font-size: var(--font-size-sm); }
.summary-value { font-weight: 500; font-size: var(--font-size-sm); }
.success-message {
text-align: center; padding: var(--spacing-2xl);
}
.success-message h2 { color: var(--success); margin-bottom: var(--spacing-md); }
/* Draft resume banner */
.draft-banner {
background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius); padding: var(--spacing-md) var(--spacing-lg);
margin-bottom: var(--spacing-lg); display: flex; justify-content: space-between; align-items: center;
}
.draft-banner-text { font-size: var(--font-size-sm); color: var(--text-primary); }
.draft-banner-actions { display: flex; gap: var(--spacing-sm); }
/* Field validation checkmarks */
.form-group { position: relative; }
.field-check {
position: absolute; right: 8px; top: 50%;
color: var(--success); font-size: 18px;
display: none; pointer-events: none;
}
.form-group.has-nip-row .field-check { right: 140px; }
.form-group.validated .field-check { display: block; }
.form-control.validated { border-color: var(--success); padding-right: 32px; }
@media (max-width: 768px) {
.wizard-steps { gap: var(--spacing-sm); }
.wizard-step span { display: none; }
.wizard-content { padding: var(--spacing-lg); }
.form-row { grid-template-columns: 1fr; }
.logo-gallery { grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); }
.audit-cards { grid-template-columns: 1fr; }
.summary-grid { grid-template-columns: 1fr; }
}
</style>
{% endblock %}
{% block content %}
<div class="wizard-container">
<div class="wizard-header">
<h1>Kreator Dodawania Firmy</h1>
<div class="wizard-steps" id="wizardSteps">
<div class="wizard-step active" data-step="1">
<div class="step-number">1</div>
<span>NIP</span>
</div>
<div class="wizard-step" data-step="2">
<div class="step-number">2</div>
<span>Dane</span>
</div>
<div class="wizard-step" data-step="3">
<div class="step-number">3</div>
<span>Logo</span>
</div>
<div class="wizard-step" data-step="4">
<div class="step-number">4</div>
<span>Audyty</span>
</div>
<div class="wizard-step" data-step="5">
<div class="step-number">5</div>
<span>Zapis</span>
</div>
</div>
</div>
{% if draft %}
<div class="draft-banner" id="draftBanner">
<div class="draft-banner-text">
Masz nieukończony szkic: <strong>{{ draft.name }}</strong> (NIP: {{ draft.nip }}, krok {{ draft.step }})
</div>
<div class="draft-banner-actions">
<button class="btn-wizard btn-primary" onclick="resumeDraft({{ draft.id }}, {{ draft.step }})">Wznów</button>
<button class="btn-wizard btn-secondary" onclick="cancelDraft({{ draft.id }})">Usuń szkic</button>
</div>
</div>
{% endif %}
<div class="wizard-content">
<!-- ==================== STEP 1: NIP ==================== -->
<div class="step-panel active" id="step1">
<div class="form-section">
<h2>Wyszukaj firmę po NIP</h2>
<p class="form-hint" style="margin-bottom: var(--spacing-lg)">
Wpisz NIP firmy. System automatycznie pobierze dane z KRS lub CEIDG.
</p>
<div class="form-group">
<label>NIP <span class="required">*</span></label>
<div class="nip-input-row">
<input type="text" id="nipInput" class="form-control"
placeholder="np. 5851491213" maxlength="10"
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
<button class="btn-wizard btn-primary" id="btnCheckNip" onclick="checkNip()">
Sprawdź
</button>
</div>
</div>
<div id="nipStatus" style="display:none"></div>
<div id="registryPreview" style="display:none"></div>
</div>
<div class="wizard-actions">
<a href="{{ url_for('admin.admin_companies') }}" class="btn-wizard btn-secondary">Anuluj</a>
<button class="btn-wizard btn-primary" id="btnStep1Next" disabled onclick="goToStep(2)">
Dalej
</button>
</div>
</div>
<!-- ==================== STEP 2: EDIT DATA ==================== -->
<div class="step-panel" id="step2">
<div class="form-section">
<h2>Dane firmy</h2>
<p class="form-hint" style="margin-bottom: var(--spacing-lg)">
Sprawdź i popraw dane pobrane z rejestru. Pola oznaczone <span class="required">*</span> są wymagane.
</p>
<div class="form-group">
<label>Nazwa firmy <span class="required">*</span></label>
<input type="text" id="editName" class="form-control">
</div>
<div class="form-group">
<label>Nazwa prawna</label>
<input type="text" id="editLegalName" class="form-control">
</div>
<div class="form-row">
<div class="form-group">
<label>Kategoria</label>
<select id="editCategory" class="form-control">
<option value="">-- Wybierz --</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Forma prawna</label>
<input type="text" id="editLegalForm" class="form-control" readonly
style="background: var(--background);">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>NIP</label>
<input type="text" id="editNip" class="form-control" readonly
style="background: var(--background);">
</div>
<div class="form-group">
<label>REGON</label>
<input type="text" id="editRegon" class="form-control" readonly
style="background: var(--background);">
</div>
<div class="form-group">
<label>KRS</label>
<input type="text" id="editKrs" class="form-control" readonly
style="background: var(--background);">
</div>
</div>
<h2 style="margin-top: var(--spacing-xl)">Adres</h2>
<div class="form-group">
<label>Ulica i numer</label>
<input type="text" id="editStreet" class="form-control">
</div>
<div class="form-row">
<div class="form-group">
<label>Kod pocztowy</label>
<input type="text" id="editPostal" class="form-control" placeholder="XX-XXX">
</div>
<div class="form-group">
<label>Miasto</label>
<input type="text" id="editCity" class="form-control">
</div>
</div>
<h2 style="margin-top: var(--spacing-xl)">Kontakt</h2>
<div class="form-row">
<div class="form-group">
<label>Email</label>
<input type="email" id="editEmail" class="form-control">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="text" id="editPhone" class="form-control">
</div>
</div>
<div class="form-group">
<label>Strona WWW</label>
<div class="nip-input-row">
<input type="text" id="editWebsite" class="form-control" placeholder="https://">
<button class="btn-wizard btn-secondary" id="btnDiscoverWebsite" onclick="discoverWebsite()">
Wyszukaj w internecie
</button>
</div>
<div id="discoverStatus" style="display:none"></div>
</div>
<div class="form-group">
<label>Krótki opis</label>
<textarea id="editDescShort" class="form-control" rows="3"
placeholder="Krótki opis firmy (widoczny w katalogu)"></textarea>
</div>
<!-- Read-only section: PKD, people -->
<div id="readOnlySection" style="display:none">
<h2 style="margin-top: var(--spacing-xl)">Dane z rejestru (tylko do odczytu)</h2>
<div id="pkdList" class="form-hint"></div>
<div id="peopleList" class="form-hint" style="margin-top: var(--spacing-sm)"></div>
</div>
</div>
<div class="wizard-actions">
<button class="btn-wizard btn-secondary" onclick="goToStep(1)">Wstecz</button>
<button class="btn-wizard btn-primary" id="btnStep2Next" onclick="saveStep2()">
Dalej
</button>
</div>
</div>
<!-- ==================== STEP 3: LOGO ==================== -->
<div class="step-panel" id="step3">
<div class="form-section">
<h2>Strona internetowa i logo</h2>
<p class="form-hint" style="margin-bottom: var(--spacing-lg)">
Pobierz logo ze strony internetowej firmy. Możesz też pominąć ten krok.
</p>
<div class="form-group">
<label>Adres strony WWW</label>
<div class="nip-input-row">
<input type="text" id="logoWebsite" class="form-control" placeholder="https://">
<button class="btn-wizard btn-primary" id="btnFetchLogos" onclick="fetchLogos()">
Pobierz logo
</button>
</div>
</div>
<div id="logoStatus" style="display:none"></div>
<div id="logoGallery" class="logo-gallery" style="display:none"></div>
<div id="logoActions" style="display:none; margin-top: var(--spacing-md)">
<button class="btn-wizard btn-success" id="btnSelectLogo" onclick="selectLogo()">
Zatwierdz wybrane logo
</button>
</div>
</div>
<div class="wizard-actions">
<button class="btn-wizard btn-secondary" onclick="goToStep(2)">Wstecz</button>
<div>
<button class="btn-link" onclick="skipStep3()" style="margin-right: var(--spacing-lg)">Pomiń logo</button>
<button class="btn-wizard btn-primary" id="btnStep3Next" onclick="goToStep(4)" disabled>
Dalej
</button>
</div>
</div>
</div>
<!-- ==================== STEP 4: AUDITS ==================== -->
<div class="step-panel" id="step4">
<div class="form-section">
<h2>Audyty</h2>
<p class="form-hint" style="margin-bottom: var(--spacing-lg)">
Uruchom automatyczne audyty SEO, Google Business Profile i Social Media.
Możesz też pominąć ten krok — audyty można uruchomić później.
</p>
<div class="audit-cards" id="auditCards">
<div class="audit-card" id="auditSeo">
<div class="audit-card-header">
<span class="audit-icon">&#128269;</span>
<h3>SEO</h3>
</div>
<div class="audit-status pending" id="auditSeoStatus">Oczekuje...</div>
<div class="audit-score" id="auditSeoScore" style="display:none"></div>
</div>
<div class="audit-card" id="auditGbp">
<div class="audit-card-header">
<span class="audit-icon">&#128205;</span>
<h3>Google Business</h3>
</div>
<div class="audit-status pending" id="auditGbpStatus">Oczekuje...</div>
<div class="audit-score" id="auditGbpScore" style="display:none"></div>
</div>
<div class="audit-card" id="auditSocial">
<div class="audit-card-header">
<span class="audit-icon">&#128241;</span>
<h3>Social Media</h3>
</div>
<div class="audit-status pending" id="auditSocialStatus">Oczekuje...</div>
<div class="audit-score" id="auditSocialScore" style="display:none"></div>
</div>
</div>
<div style="text-align: center; margin-top: var(--spacing-lg)">
<button class="btn-wizard btn-primary" id="btnStartAudits" onclick="startAudits()">
Uruchom wszystkie audyty
</button>
</div>
</div>
<div class="wizard-actions">
<button class="btn-wizard btn-secondary" onclick="goToStep(3)">Wstecz</button>
<div>
<button class="btn-link" onclick="skipStep4()" style="margin-right: var(--spacing-lg)">Pomiń audyty</button>
<button class="btn-wizard btn-primary" id="btnStep4Next" onclick="goToStep(5)">
Dalej
</button>
</div>
</div>
</div>
<!-- ==================== STEP 5: SUMMARY ==================== -->
<div class="step-panel" id="step5">
<div class="form-section" id="summarySection">
<h2>Podsumowanie</h2>
<p class="form-hint" style="margin-bottom: var(--spacing-lg)">
Sprawdź dane firmy przed zapisaniem. Po zatwierdzeniu firma pojawi się na portalu.
</p>
<div class="summary-grid" id="summaryGrid">
<!-- Filled by JS -->
</div>
<div class="form-row" style="margin-top: var(--spacing-xl)">
<div class="form-group">
<label>Status firmy</label>
<select id="finalStatus" class="form-control">
<option value="active">Aktywna (widoczna na portalu)</option>
<option value="pending">Oczekująca (ukryta)</option>
</select>
</div>
<div class="form-group">
<label>Kategoria</label>
<select id="finalCategory" class="form-control">
<option value="">-- Bez zmian --</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="success-message" id="successMessage" style="display:none">
<h2>Firma dodana pomyślnie!</h2>
<p id="successDetails"></p>
<div style="margin-top: var(--spacing-lg); display: flex; justify-content: center; gap: var(--spacing-md);">
<a id="linkToCompany" href="#" class="btn-wizard btn-primary">Zobacz profil firmy</a>
<a href="{{ url_for('admin.admin_companies') }}" class="btn-wizard btn-secondary">Lista firm</a>
<a href="{{ url_for('admin.company_wizard') }}" class="btn-wizard btn-success">Dodaj kolejną</a>
</div>
</div>
<div class="wizard-actions" id="finalActions">
<button class="btn-wizard btn-secondary" onclick="goToStep(4)">Wstecz</button>
<button class="btn-wizard btn-success" id="btnFinalize" onclick="finalize()">
Zapisz firme
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
var wizardState = {
companyId: null,
currentStep: 1,
registrySource: null,
companyData: {},
selectedLogoIndex: null,
auditPollTimer: null
};
// ==================== NAVIGATION ====================
function goToStep(step) {
// Hide all panels
document.querySelectorAll('.step-panel').forEach(function(p) { p.classList.remove('active'); });
document.getElementById('step' + step).classList.add('active');
// Update step indicators
document.querySelectorAll('.wizard-step').forEach(function(s) {
var sn = parseInt(s.dataset.step);
s.classList.remove('active', 'completed');
if (sn === step) s.classList.add('active');
else if (sn < step) s.classList.add('completed');
});
wizardState.currentStep = step;
// Step-specific init
if (step === 2) populateEditForm();
if (step === 3) document.getElementById('logoWebsite').value = wizardState.companyData.website || '';
if (step === 5) buildSummary();
}
// ==================== STEP 1: NIP CHECK ====================
function checkNip() {
var nip = document.getElementById('nipInput').value.trim().replace(/[-\s]/g, '');
if (nip.length !== 10) {
showNipStatus('Wpisz 10 cyfr NIP', 'error');
return;
}
var btn = document.getElementById('btnCheckNip');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Sprawdzam...';
showNipStatus('Szukam firmy w rejestrach KRS i CEIDG\u2026', 'loading');
document.getElementById('registryPreview').style.display = 'none';
fetch('/admin/companies/wizard/step1', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({nip: nip})
})
.then(function(r) { return r.json().then(function(d) { return {ok: r.ok, data: d}; }); })
.then(function(res) {
btn.disabled = false;
btn.innerHTML = 'Sprawdź';
if (!res.ok || !res.data.success) {
var errMsg = res.data.error || 'Nieznany b\u0142\u0105d';
showNipStatus(errMsg, 'error');
if (res.data.existing_id) {
document.getElementById('nipStatus').innerHTML +=
' <a href="/admin/companies/' + res.data.existing_id + '/detail" style="color:var(--primary)">' +
'Zobacz istniejaca firme</a>';
}
return;
}
var c = res.data.company;
wizardState.companyId = c.id;
wizardState.companyData = c;
wizardState.registrySource = c.registry_source;
var sourceLabel = c.registry_source === 'KRS' ? 'KRS' :
c.registry_source === 'CEIDG' ? 'CEIDG' : 'Ręczne wprowadzanie';
var sourceClass = (c.registry_source || 'manual').toLowerCase();
showNipStatus('Znaleziono firmę w rejestrze', 'success');
showRegistryPreview(c, sourceLabel, sourceClass);
document.getElementById('btnStep1Next').disabled = false;
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Sprawdź';
showNipStatus('Błąd połączenia: ' + err.message, 'error');
});
}
function showNipStatus(msg, type) {
var el = document.getElementById('nipStatus');
el.style.display = 'block';
el.className = 'lookup-status ' + type;
el.innerHTML = (type === 'loading' ? '<span class="spinner"></span> ' : '') + msg;
}
function showRegistryPreview(c, sourceLabel, sourceClass) {
var rows = '';
if (c.name) rows += '<div class="registry-data-row"><span class="registry-data-label">Nazwa:</span><span>' + esc(c.name) + '</span></div>';
if (c.legal_name && c.legal_name !== c.name) rows += '<div class="registry-data-row"><span class="registry-data-label">Nazwa prawna:</span><span>' + esc(c.legal_name) + '</span></div>';
if (c.nip) rows += '<div class="registry-data-row"><span class="registry-data-label">NIP:</span><span>' + c.nip + '</span></div>';
if (c.regon) rows += '<div class="registry-data-row"><span class="registry-data-label">REGON:</span><span>' + c.regon + '</span></div>';
if (c.krs) rows += '<div class="registry-data-row"><span class="registry-data-label">KRS:</span><span>' + c.krs + '</span></div>';
if (c.address_full) rows += '<div class="registry-data-row"><span class="registry-data-label">Adres:</span><span>' + esc(c.address_full) + '</span></div>';
if (c.legal_form) rows += '<div class="registry-data-row"><span class="registry-data-label">Forma:</span><span>' + esc(c.legal_form) + '</span></div>';
if (c.website) rows += '<div class="registry-data-row"><span class="registry-data-label">WWW:</span><span>' + esc(c.website) + '</span></div>';
if (c.email) rows += '<div class="registry-data-row"><span class="registry-data-label">Email:</span><span>' + esc(c.email) + '</span></div>';
if (c.phone) rows += '<div class="registry-data-row"><span class="registry-data-label">Telefon:</span><span>' + esc(c.phone) + '</span></div>';
if (c.pkd_codes && c.pkd_codes.length) {
var pkdText = c.pkd_codes.map(function(p) {
return p.code + ' ' + p.description + (p.is_primary ? ' (gl.)' : '');
}).join(', ');
rows += '<div class="registry-data-row"><span class="registry-data-label">PKD:</span><span>' + esc(pkdText) + '</span></div>';
}
if (c.people && c.people.length) {
var pplText = c.people.map(function(p) { return p.name + ' — ' + p.role; }).join(', ');
rows += '<div class="registry-data-row"><span class="registry-data-label">Zarzad:</span><span>' + esc(pplText) + '</span></div>';
}
var html = '<div class="registry-preview success">' +
'<h4><span class="status-badge ' + sourceClass + '">' + sourceLabel + '</span> Dane z rejestru</h4>' +
'<div class="registry-data">' + rows + '</div></div>';
var el = document.getElementById('registryPreview');
el.style.display = 'block';
el.innerHTML = html;
}
// ==================== STEP 2: EDIT FORM ====================
function populateEditForm() {
var c = wizardState.companyData;
document.getElementById('editName').value = c.name || '';
document.getElementById('editLegalName').value = c.legal_name || '';
document.getElementById('editNip').value = c.nip || '';
document.getElementById('editRegon').value = c.regon || '';
document.getElementById('editKrs').value = c.krs || '';
document.getElementById('editLegalForm').value = c.legal_form || '';
document.getElementById('editStreet').value = c.address_street || '';
document.getElementById('editPostal').value = c.address_postal || '';
document.getElementById('editCity').value = c.address_city || '';
document.getElementById('editEmail').value = c.email || '';
document.getElementById('editPhone').value = c.phone || '';
var www = c.website || '';
if (www === 'https://' || www === 'http://') www = '';
document.getElementById('editWebsite').value = www;
document.getElementById('editDescShort').value = c.description_short || '';
if (c.category_id) document.getElementById('editCategory').value = c.category_id;
// PKD codes
var pkdEl = document.getElementById('pkdList');
var roSection = document.getElementById('readOnlySection');
if (c.pkd_codes && c.pkd_codes.length) {
pkdEl.innerHTML = '<strong>Kody PKD:</strong> ' + c.pkd_codes.map(function(p) {
return p.code + ' ' + p.description;
}).join(' | ');
roSection.style.display = 'block';
}
if (c.people && c.people.length) {
document.getElementById('peopleList').innerHTML = '<strong>Zarząd:</strong> ' + c.people.map(function(p) {
return p.name + ' (' + p.role + ')';
}).join(' | ');
roSection.style.display = 'block';
}
// Show hint if contact data is missing from registry
var missing = [];
if (!c.website) missing.push('strony WWW');
if (!c.email) missing.push('adresu email');
if (!c.phone) missing.push('telefonu');
if (missing.length > 0) {
var hint = document.getElementById('discoverStatus');
if (hint) {
hint.style.display = 'block';
hint.className = 'lookup-status';
hint.innerHTML = 'Rejestr ' + (wizardState.registrySource || 'KRS') +
' nie zawiera: ' + missing.join(', ') +
'. Użyj przycisku <strong>Wyszukaj stronę</strong> lub uzupełnij ręcznie.';
}
}
validateFields();
}
function validateFields() {
var checks = [
{id: 'editName', ok: function(v) { return v.length > 1 && !v.startsWith('Firma NIP'); }},
{id: 'editLegalName', ok: function(v) { return v.length > 1; }},
{id: 'editNip', ok: function(v) { return /^\d{10}$/.test(v); }},
{id: 'editRegon', ok: function(v) { return /^\d{9,14}$/.test(v); }},
{id: 'editKrs', ok: function(v) { return /^\d{10}$/.test(v); }},
{id: 'editLegalForm', ok: function(v) { return v.length > 2; }},
{id: 'editStreet', ok: function(v) { return v.length > 2; }},
{id: 'editPostal', ok: function(v) { return /^\d{2}-\d{3}$/.test(v); }},
{id: 'editCity', ok: function(v) { return v.length > 1; }},
{id: 'editEmail', ok: function(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }},
{id: 'editPhone', ok: function(v) { return v.length >= 7; }},
{id: 'editWebsite', ok: function(v) { return v.length > 8 && v !== 'https://'; }},
];
checks.forEach(function(c) {
var el = document.getElementById(c.id);
if (!el) return;
var val = el.value.trim();
var group = el.closest('.form-group');
if (!group) return;
// Add checkmark span if not exists
var checkEl = group.querySelector('.field-check');
if (!checkEl) {
checkEl = document.createElement('span');
checkEl.className = 'field-check';
checkEl.innerHTML = '&#10003;';
el.parentNode.style.position = 'relative';
el.parentNode.appendChild(checkEl);
}
if (val && c.ok(val)) {
group.classList.add('validated');
el.classList.add('validated');
} else {
group.classList.remove('validated');
el.classList.remove('validated');
}
});
}
// Live validation on input
document.addEventListener('input', function(e) {
if (e.target.closest('#step2')) validateFields();
});
function discoverWebsite() {
// If website already filled, just confirm it
var existing = document.getElementById('editWebsite').value.trim();
if (existing && existing !== 'https://' && existing.length > 10) {
var statusEl = document.getElementById('discoverStatus');
statusEl.style.display = 'block';
statusEl.className = 'lookup-status success';
statusEl.innerHTML = 'Strona WWW już pobrana z rejestru: <strong>' + esc(existing) + '</strong>';
return;
}
var btn = document.getElementById('btnDiscoverWebsite');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Szukam...';
var statusEl = document.getElementById('discoverStatus');
statusEl.style.display = 'block';
statusEl.className = 'lookup-status loading';
statusEl.innerHTML = '<span class="spinner"></span> Szukam strony w internecie...';
fetch('/admin/companies/wizard/discover-website', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId})
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = 'Wyszukaj w internecie';
if (data.success && data.website) {
document.getElementById('editWebsite').value = data.website;
wizardState.companyData.website = data.website;
statusEl.className = 'lookup-status success';
statusEl.innerHTML = 'Znaleziono: ' + esc(data.website);
} else {
statusEl.className = 'lookup-status error';
statusEl.innerHTML = data.error || 'Nie znaleziono strony internetowej';
}
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Wyszukaj w internecie';
statusEl.className = 'lookup-status error';
statusEl.innerHTML = 'Błąd: ' + err.message;
});
}
function saveStep2() {
var btn = document.getElementById('btnStep2Next');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Zapisuje...';
var payload = {
company_id: wizardState.companyId,
name: document.getElementById('editName').value.trim(),
legal_name: document.getElementById('editLegalName').value.trim(),
category_id: document.getElementById('editCategory').value,
address_street: document.getElementById('editStreet').value.trim(),
address_postal: document.getElementById('editPostal').value.trim(),
address_city: document.getElementById('editCity').value.trim(),
email: document.getElementById('editEmail').value.trim(),
phone: document.getElementById('editPhone').value.trim(),
website: document.getElementById('editWebsite').value.trim(),
description_short: document.getElementById('editDescShort').value.trim()
};
if (!payload.name) {
alert('Nazwa firmy jest wymagana');
btn.disabled = false;
btn.innerHTML = 'Dalej';
return;
}
fetch('/admin/companies/wizard/step2', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = 'Dalej';
if (data.success) {
wizardState.companyData = data.company;
goToStep(3);
} else {
alert(data.error || 'Błąd zapisu');
}
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Dalej';
alert('Błąd połączenia: ' + err.message);
});
}
// ==================== STEP 3: LOGO ====================
function fetchLogos() {
var btn = document.getElementById('btnFetchLogos');
var url = document.getElementById('logoWebsite').value.trim();
if (!url) { alert('Podaj adres strony WWW'); return; }
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Pobieram...';
showLogoStatus('Szukam logo na stronie\u2026', 'loading');
fetch('/admin/companies/wizard/step3/fetch-logos', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId, website_url: url})
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = 'Pobierz logo';
if (!data.success) {
showLogoStatus(data.error || 'Nie udało się pobrać logo', 'error');
return;
}
var candidates = data.candidates || [];
if (candidates.length === 0) {
showLogoStatus('Nie znaleziono logo na stronie', 'error');
return;
}
showLogoStatus('Znaleziono ' + candidates.length + ' kandydatów', 'success');
renderLogoGallery(candidates, data.recommended_index || 0);
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Pobierz logo';
showLogoStatus('Błąd: ' + err.message, 'error');
});
}
function showLogoStatus(msg, type) {
var el = document.getElementById('logoStatus');
el.style.display = 'block';
el.className = 'lookup-status ' + type;
el.innerHTML = (type === 'loading' ? '<span class="spinner"></span> ' : '') + msg;
}
function renderLogoGallery(candidates, recommendedIdx) {
var gallery = document.getElementById('logoGallery');
gallery.style.display = 'grid';
gallery.innerHTML = '';
var slug = wizardState.companyData.slug;
candidates.forEach(function(cand, i) {
var ext = cand.ext || 'webp';
var src = '/static/img/companies/' + slug + '_cand_' + i + '.' + ext + '?t=' + Date.now();
var div = document.createElement('div');
div.className = 'logo-candidate' + (i === recommendedIdx ? ' recommended' : '') + (i === recommendedIdx ? ' selected' : '');
div.innerHTML = '<img src="' + src + '" alt="Kandydat ' + (i + 1) + '" onerror="this.src=\'/static/img/placeholder.png\'">' +
'<div class="label">' + (cand.source || cand.label || 'Logo ' + (i + 1)) + '</div>';
div.onclick = function() { selectLogoCandidate(i); };
gallery.appendChild(div);
});
wizardState.selectedLogoIndex = recommendedIdx;
document.getElementById('logoActions').style.display = 'block';
}
function selectLogoCandidate(idx) {
wizardState.selectedLogoIndex = idx;
document.querySelectorAll('.logo-candidate').forEach(function(el, i) {
el.classList.toggle('selected', i === idx);
});
}
function selectLogo() {
if (wizardState.selectedLogoIndex === null) { alert('Wybierz logo'); return; }
var btn = document.getElementById('btnSelectLogo');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Zapisuje...';
fetch('/admin/companies/wizard/step3/select-logo', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId, candidate_index: wizardState.selectedLogoIndex})
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = 'Zatwierdz wybrane logo';
if (data.success) {
showLogoStatus('Logo zapisane! Przechodze dalej...', 'success');
document.getElementById('btnStep3Next').disabled = false;
setTimeout(function() { goToStep(4); }, 1000);
wizardState.companyData.logo_path = data.logo_path;
} else {
alert(data.error || 'Błąd zapisu logo');
}
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Zatwierdz wybrane logo';
alert('Błąd: ' + err.message);
});
}
function skipStep3() {
fetch('/admin/companies/wizard/step3/skip', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId})
}).then(function() { goToStep(4); });
}
// ==================== STEP 4: AUDITS ====================
function startAudits() {
var btn = document.getElementById('btnStartAudits');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Uruchamiam...';
fetch('/admin/companies/wizard/step4/start-audits', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
updateAuditUI(data.audit_status);
wizardState.auditPollTimer = setInterval(pollAuditStatus, 3000);
} else {
btn.disabled = false;
btn.innerHTML = 'Uruchom wszystkie audyty';
alert(data.error || 'Błąd uruchomienia audytów');
}
});
}
function pollAuditStatus() {
fetch('/admin/companies/wizard/step4/audit-status?company_id=' + wizardState.companyId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
updateAuditUI(data.audit_status);
if (data.all_done) {
clearInterval(wizardState.auditPollTimer);
wizardState.auditPollTimer = null;
document.getElementById('btnStartAudits').style.display = 'none';
}
}
});
}
function updateAuditUI(status) {
var mapping = {
pending: {label: 'Oczekuje...', cls: 'pending'},
running: {label: '<span class="spinner"></span> Trwa...', cls: 'running'},
completed: {label: 'Gotowe', cls: 'completed'},
skipped: {label: 'Pominięto', cls: 'skipped'},
not_found: {label: 'Nie znaleziono', cls: 'skipped'}
};
['seo', 'gbp', 'social'].forEach(function(key) {
var val = status[key] || 'pending';
var statusEl = document.getElementById('audit' + capitalize(key) + 'Status');
var scoreEl = document.getElementById('audit' + capitalize(key) + 'Score');
if (val.startsWith('error')) {
statusEl.className = 'audit-status error';
statusEl.innerHTML = 'Błąd';
} else {
var m = mapping[val] || mapping.pending;
statusEl.className = 'audit-status ' + m.cls;
statusEl.innerHTML = m.label;
}
// Show score
var scoreKey = key + '_score';
if (key === 'social') scoreKey = 'social_count';
if (status[scoreKey] !== undefined) {
scoreEl.style.display = 'block';
if (key === 'social') {
scoreEl.textContent = status[scoreKey] + ' profili';
} else {
scoreEl.textContent = status[scoreKey] + '%';
}
}
});
}
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
function skipStep4() {
if (wizardState.auditPollTimer) {
clearInterval(wizardState.auditPollTimer);
wizardState.auditPollTimer = null;
}
fetch('/admin/companies/wizard/step4/skip', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: wizardState.companyId})
}).then(function() { goToStep(5); });
}
// ==================== STEP 5: SUMMARY ====================
function buildSummary() {
var c = wizardState.companyData;
var grid = document.getElementById('summaryGrid');
var companyCard = '<div class="summary-card"><h3>Dane firmy</h3>' +
summaryRow('Nazwa', c.name) +
summaryRow('Nazwa prawna', c.legal_name) +
summaryRow('NIP', c.nip) +
summaryRow('REGON', c.regon) +
summaryRow('KRS', c.krs) +
summaryRow('Forma', c.legal_form) +
summaryRow('PKD', c.pkd_code) +
summaryRow('Źródło danych', wizardState.registrySource) +
'</div>';
var addressCard = '<div class="summary-card"><h3>Adres i kontakt</h3>' +
summaryRow('Ulica', c.address_street) +
summaryRow('Kod', c.address_postal) +
summaryRow('Miasto', c.address_city) +
summaryRow('Email', c.email) +
summaryRow('Telefon', c.phone) +
summaryRow('WWW', c.website) +
'</div>';
var logoHtml = '';
if (c.logo_path) {
logoHtml = '<div style="text-align:center;padding:var(--spacing-md);background:var(--background);border-radius:var(--radius);margin-top:var(--spacing-sm)"><img src="' + c.logo_path + '?t=' + Date.now() + '" style="max-width:140px;max-height:100px;object-fit:contain"></div>';
}
var extrasCard = '<div class="summary-card"><h3>Logo i audyty</h3>' +
summaryRow('Logo', c.logo_path ? 'Tak' : 'Brak') + logoHtml +
summaryRow('Opis', c.description_short ? 'Tak (' + c.description_short.length + ' zn.)' : 'Brak') +
'</div>';
grid.innerHTML = companyCard + addressCard + extrasCard;
// Set category from step 2
if (c.category_id) {
document.getElementById('finalCategory').value = c.category_id;
}
}
function summaryRow(label, value) {
return '<div class="summary-row"><span class="summary-label">' + label + '</span>' +
'<span class="summary-value">' + (value ? esc(String(value)) : '<em style="color:var(--text-secondary)"></em>') + '</span></div>';
}
function finalize() {
var btn = document.getElementById('btnFinalize');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Zapisuje...';
fetch('/admin/companies/wizard/step5/finalize', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({
company_id: wizardState.companyId,
status: document.getElementById('finalStatus').value,
category_id: document.getElementById('finalCategory').value
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
document.getElementById('summarySection').style.display = 'none';
document.getElementById('finalActions').style.display = 'none';
var msg = document.getElementById('successMessage');
msg.style.display = 'block';
document.getElementById('successDetails').innerHTML =
'<strong>' + esc(data.name) + '</strong> (ID: ' + data.company_id + ')<br>' +
'Status: ' + data.status + ' | Jakość danych: ' + data.data_quality +
' (' + data.data_quality_score + '%)';
document.getElementById('linkToCompany').href = '/company/' + data.slug;
// Mark all steps as completed
document.querySelectorAll('.wizard-step').forEach(function(s) {
s.classList.remove('active');
s.classList.add('completed');
});
} else {
btn.disabled = false;
btn.innerHTML = 'Zapisz firme';
alert(data.error || 'Błąd zapisu');
}
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = 'Zapisz firme';
alert('Błąd: ' + err.message);
});
}
// ==================== DRAFT RESUME/CANCEL ====================
function resumeDraft(companyId, step) {
wizardState.companyId = companyId;
// Fetch company data
fetch('/admin/companies/' + companyId, {headers: {'Accept': 'application/json'}})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.company) {
wizardState.companyData = data.company;
wizardState.registrySource = data.company.data_source || 'manual';
}
document.getElementById('draftBanner').style.display = 'none';
goToStep(Math.max(step, 2));
})
.catch(function() {
goToStep(step);
});
}
function cancelDraft(companyId) {
if (!confirm('Czy na pewno chcesz usunac ten szkic?')) return;
fetch('/admin/companies/wizard/cancel', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
body: JSON.stringify({company_id: companyId})
}).then(function() {
document.getElementById('draftBanner').style.display = 'none';
});
}
// ==================== UTILITIES ====================
function getCSRF() {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
function esc(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// NIP input: enter to check
document.getElementById('nipInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') { e.preventDefault(); checkNip(); }
});
{% endblock %}