nordabiz/templates/membership/apply.html
Maciej Pienczyn 0f8aca1435 feat: Add membership application system
Implement full online membership application workflow:
- 3-step wizard form with KRS/CEIDG auto-fill
- Admin panel for application review (approve/reject/request changes)
- Company data update requests for existing members
- Dashboard CTA for users without company
- API endpoints for NIP lookup and draft management

New files:
- database/migrations/042_membership_applications.sql
- blueprints/membership/ (routes, templates)
- blueprints/admin/routes_membership.py
- blueprints/api/routes_membership.py
- templates/membership/ and templates/admin/membership*.html

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 12:38:31 +01:00

763 lines
28 KiB
HTML

{% extends "base.html" %}
{% block title %}Deklaracja Członkowska - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.wizard-container {
max-width: 800px;
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);
}
.wizard-step {
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
}
.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;
}
.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);
}
.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);
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
}
.form-control.error {
border-color: var(--error);
}
.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-lookup {
display: flex;
gap: var(--spacing-sm);
}
.nip-lookup .form-control {
flex: 1;
}
.btn-lookup {
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--surface);
border: 1px solid var(--primary);
color: var(--primary);
border-radius: var(--radius);
cursor: pointer;
font-weight: 500;
transition: var(--transition);
white-space: nowrap;
}
.btn-lookup:hover {
background: var(--primary);
color: white;
}
.btn-lookup:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.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.05);
}
.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: 100px;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.checkbox-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary);
}
.radio-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-sm);
}
.radio-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--background);
border-radius: var(--radius);
}
.radio-item input[type="radio"] {
accent-color: var(--primary);
}
.declaration-box {
background: var(--background);
border: 2px solid var(--border);
border-radius: var(--radius);
padding: var(--spacing-lg);
margin-top: var(--spacing-lg);
}
.declaration-box.accepted {
border-color: var(--success);
background: rgba(var(--success-rgb), 0.05);
}
.declaration-text {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
line-height: 1.6;
}
.wizard-buttons {
display: flex;
justify-content: space-between;
margin-top: var(--spacing-xl);
padding-top: var(--spacing-xl);
border-top: 1px solid var(--border);
}
.btn-wizard {
padding: var(--spacing-md) var(--spacing-xl);
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-prev {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text-primary);
}
.btn-prev:hover {
background: var(--background);
}
.btn-next, .btn-submit {
background: var(--primary);
border: none;
color: white;
}
.btn-next:hover, .btn-submit:hover {
opacity: 0.9;
}
.btn-save {
background: var(--surface);
border: 1px solid var(--success);
color: var(--success);
}
.btn-save:hover {
background: rgba(var(--success-rgb), 0.1);
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.wizard-steps {
flex-direction: column;
align-items: flex-start;
}
.wizard-buttons {
flex-direction: column-reverse;
gap: var(--spacing-md);
}
.wizard-buttons button {
width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<div class="wizard-container">
<div class="wizard-header">
<h1>Deklaracja Członkowska</h1>
<p class="text-muted">Przystąpienie do Izby Przedsiębiorców NORDA</p>
<div class="wizard-steps">
<div class="wizard-step {% if step == 1 %}active{% elif step > 1 %}completed{% endif %}">
<span class="step-number">{% if step > 1 %}✓{% else %}1{% endif %}</span>
<span>Dane firmy</span>
</div>
<div class="wizard-step {% if step == 2 %}active{% elif step > 2 %}completed{% endif %}">
<span class="step-number">{% if step > 2 %}✓{% else %}2{% endif %}</span>
<span>Informacje</span>
</div>
<div class="wizard-step {% if step == 3 %}active{% endif %}">
<span class="step-number">3</span>
<span>Sekcje i zgody</span>
</div>
</div>
</div>
<div class="wizard-content">
<form method="POST" id="wizardForm">
{% if step == 1 %}
<!-- STEP 1: Dane firmy -->
<div class="form-section">
<h2>Pobierz dane z rejestru</h2>
<div class="form-group">
<label>NIP <span class="required">*</span></label>
<div class="nip-lookup">
<input type="text" class="form-control" id="nipInput" name="nip"
value="{{ application.nip or '' }}"
placeholder="0000000000" maxlength="10" pattern="\d{10}">
<button type="button" class="btn-lookup" id="btnLookup">
Sprawdź w rejestrze
</button>
</div>
<div class="form-hint">Wpisz 10-cyfrowy NIP bez myślników</div>
</div>
<div id="registryPreview" class="registry-preview" style="display: none;">
<h4>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22,4 12,14.01 9,11.01"/>
</svg>
Dane pobrane z <span id="registrySource">KRS</span>
</h4>
<div class="registry-data" id="registryData"></div>
</div>
</div>
<div class="form-section">
<h2>Dane firmy</h2>
<div class="form-group">
<label>Nazwa firmy <span class="required">*</span></label>
<input type="text" class="form-control" name="company_name"
value="{{ application.company_name or '' }}" required>
</div>
<div class="form-row">
<div class="form-group">
<label>Kod pocztowy</label>
<input type="text" class="form-control" name="address_postal_code"
value="{{ application.address_postal_code or '' }}"
placeholder="00-000" maxlength="6">
</div>
<div class="form-group">
<label>Miejscowość</label>
<input type="text" class="form-control" name="address_city"
value="{{ application.address_city or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Ulica</label>
<input type="text" class="form-control" name="address_street"
value="{{ application.address_street or '' }}">
</div>
<div class="form-group">
<label>Nr budynku/lokalu</label>
<input type="text" class="form-control" name="address_number"
value="{{ application.address_number or '' }}">
</div>
</div>
<input type="hidden" name="krs_number" value="{{ application.krs_number or '' }}">
<input type="hidden" name="regon" value="{{ application.regon or '' }}">
<input type="hidden" name="registry_source" value="{{ application.registry_source or 'manual' }}">
</div>
<div class="form-section">
<h2>Delegaci do Walnego Zgromadzenia</h2>
<div class="form-hint" style="margin-bottom: var(--spacing-md);">
Wskaż osoby reprezentujące firmę na Walnym Zgromadzeniu Członków Izby (min. 1, max. 3)
</div>
<div class="form-group">
<label>Delegat 1 (główny) <span class="required">*</span></label>
<input type="text" class="form-control" name="delegate_1"
value="{{ application.delegate_1 or '' }}"
placeholder="Imię i nazwisko" required>
</div>
<div class="form-row">
<div class="form-group">
<label>Delegat 2 (opcjonalnie)</label>
<input type="text" class="form-control" name="delegate_2"
value="{{ application.delegate_2 or '' }}"
placeholder="Imię i nazwisko">
</div>
<div class="form-group">
<label>Delegat 3 (opcjonalnie)</label>
<input type="text" class="form-control" name="delegate_3"
value="{{ application.delegate_3 or '' }}"
placeholder="Imię i nazwisko">
</div>
</div>
</div>
{% elif step == 2 %}
<!-- STEP 2: Karta informacyjna -->
<div class="form-section">
<h2>Dane kontaktowe</h2>
<div class="form-row">
<div class="form-group">
<label>Strona WWW</label>
<input type="url" class="form-control" name="website"
value="{{ application.website or '' }}"
placeholder="https://przykład.pl">
</div>
<div class="form-group">
<label>Email firmowy <span class="required">*</span></label>
<input type="email" class="form-control" name="email"
value="{{ application.email or '' }}" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Telefon</label>
<input type="tel" class="form-control" name="phone"
value="{{ application.phone or '' }}"
placeholder="+48 000 000 000">
</div>
<div class="form-group">
<label>Nazwa skrócona</label>
<input type="text" class="form-control" name="short_name"
value="{{ application.short_name or '' }}"
placeholder="np. INPI">
</div>
</div>
</div>
<div class="form-section">
<h2>Informacje o firmie</h2>
<div class="form-group">
<label>Opis działalności</label>
<textarea class="form-control" name="description" rows="4"
placeholder="Krótki opis działalności firmy...">{{ application.description or '' }}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Data założenia</label>
<input type="date" class="form-control" name="founded_date"
value="{{ application.founded_date.isoformat() if application.founded_date else '' }}">
</div>
<div class="form-group">
<label>Średnie zatrudnienie</label>
<input type="number" class="form-control" name="employee_count"
value="{{ application.employee_count or '' }}" min="0">
<div class="checkbox-item" style="margin-top: var(--spacing-sm);">
<input type="checkbox" name="show_employee_count" id="showEmployeeCount"
{% if application.show_employee_count %}checked{% endif %}>
<label for="showEmployeeCount">Pokaż publicznie</label>
</div>
</div>
</div>
<div class="form-group">
<label>Średnioroczne obroty</label>
<select class="form-control" name="annual_revenue">
<option value="">Nie podaję</option>
<option value="do_100k" {% if application.annual_revenue == 'do_100k' %}selected{% endif %}>do 100 tys. PLN</option>
<option value="100k_500k" {% if application.annual_revenue == '100k_500k' %}selected{% endif %}>100 - 500 tys. PLN</option>
<option value="500k_2m" {% if application.annual_revenue == '500k_2m' %}selected{% endif %}>500 tys. - 2 mln PLN</option>
<option value="2m_10m" {% if application.annual_revenue == '2m_10m' %}selected{% endif %}>2 - 10 mln PLN</option>
<option value="10m_50m" {% if application.annual_revenue == '10m_50m' %}selected{% endif %}>10 - 50 mln PLN</option>
<option value="powyzej_50m" {% if application.annual_revenue == 'powyzej_50m' %}selected{% endif %}>powyżej 50 mln PLN</option>
</select>
</div>
</div>
<div class="form-section">
<h2>Spółki powiązane (opcjonalnie)</h2>
<div class="form-hint" style="margin-bottom: var(--spacing-md);">
Jeśli firma należy do grupy kapitałowej, podaj nazwy powiązanych spółek (max 5)
</div>
{% for i in range(1, 6) %}
<div class="form-group">
<input type="text" class="form-control" name="related_company_{{ i }}"
value="{{ application.related_companies[i-1] if application.related_companies and application.related_companies|length >= i else '' }}"
placeholder="Nazwa spółki powiązanej">
</div>
{% endfor %}
</div>
{% elif step == 3 %}
<!-- STEP 3: Sekcje i oświadczenia -->
<div class="form-section">
<h2>Sekcje tematyczne <span class="required">*</span></h2>
<div class="form-hint" style="margin-bottom: var(--spacing-md);">
Wybierz sekcje tematyczne odpowiadające profilowi działalności firmy (min. 1)
</div>
<div class="checkbox-group">
{% for value, label in section_choices %}
<div class="checkbox-item">
<input type="checkbox" name="sections" value="{{ value }}" id="section_{{ value }}"
{% if application.sections and value in application.sections %}checked{% endif %}>
<label for="section_{{ value }}">{{ label }}</label>
</div>
{% endfor %}
</div>
<div class="form-group" style="margin-top: var(--spacing-md);">
<label>Jeśli wybrano "Inna", opisz:</label>
<input type="text" class="form-control" name="sections_other"
value="{{ application.sections_other or '' }}"
placeholder="Opis innej sekcji">
</div>
</div>
<div class="form-section">
<h2>Forma prawna <span class="required">*</span></h2>
<div class="radio-group">
{% for value, label in business_type_choices %}
<div class="radio-item">
<input type="radio" name="business_type" value="{{ value }}" id="btype_{{ value }}"
{% if application.business_type == value %}checked{% endif %} required>
<label for="btype_{{ value }}">{{ label }}</label>
</div>
{% endfor %}
</div>
<div class="form-group" style="margin-top: var(--spacing-md);">
<label>Jeśli wybrano "Inna", opisz:</label>
<input type="text" class="form-control" name="business_type_other"
value="{{ application.business_type_other or '' }}"
placeholder="Opis formy prawnej">
</div>
</div>
<div class="form-section">
<h2>Zgody RODO</h2>
<div class="checkbox-item" style="margin-bottom: var(--spacing-md);">
<input type="checkbox" name="consent_email" id="consentEmail"
{% if application.consent_email %}checked{% endif %} required>
<label for="consentEmail">
Wyrażam zgodę na otrzymywanie informacji drogą elektroniczną (email) <span class="required">*</span>
</label>
</div>
<div class="form-group">
<label>Email do kontaktu</label>
<input type="email" class="form-control" name="consent_email_address"
value="{{ application.consent_email_address or application.email or '' }}"
placeholder="adres@email.pl">
</div>
<div class="checkbox-item" style="margin-bottom: var(--spacing-md);">
<input type="checkbox" name="consent_sms" id="consentSms"
{% if application.consent_sms %}checked{% endif %}>
<label for="consentSms">
Wyrażam zgodę na otrzymywanie powiadomień SMS
</label>
</div>
<div class="form-group">
<label>Telefon do SMS</label>
<input type="tel" class="form-control" name="consent_sms_phone"
value="{{ application.consent_sms_phone or application.phone or '' }}"
placeholder="+48 000 000 000">
</div>
</div>
<div class="form-section">
<h2>Oświadczenie</h2>
<div class="declaration-box {% if application.declaration_accepted %}accepted{% endif %}" id="declarationBox">
<div class="declaration-text">
Oświadczam, że zapoznałem/am się ze Statutem Izby Przedsiębiorców NORDA i zobowiązuję się do jego przestrzegania.
Jednocześnie wyrażam zgodę na przetwarzanie moich danych osobowych przez Izbę Przedsiębiorców NORDA
w celach statutowych i marketingowych. Wszystkie podane przeze mnie informacje są prawdziwe i aktualne.
</div>
<div class="checkbox-item">
<input type="checkbox" name="declaration_accepted" id="declarationAccepted"
{% if application.declaration_accepted %}checked{% endif %} required>
<label for="declarationAccepted">
<strong>Akceptuję powyższe oświadczenie</strong> <span class="required">*</span>
</label>
</div>
</div>
</div>
{% endif %}
<div class="wizard-buttons">
<div>
{% if step > 1 %}
<button type="submit" name="action" value="prev" class="btn-wizard btn-prev">
← Wstecz
</button>
{% endif %}
</div>
<div style="display: flex; gap: var(--spacing-md);">
<button type="submit" name="action" value="save" class="btn-wizard btn-save">
Zapisz
</button>
{% if step < 3 %}
<button type="submit" name="action" value="next" class="btn-wizard btn-next">
Dalej →
</button>
{% else %}
<button type="submit" name="action" value="next" class="btn-wizard btn-submit">
Wyślij deklarację
</button>
{% endif %}
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% if step == 1 %}
const nipInput = document.getElementById('nipInput');
const btnLookup = document.getElementById('btnLookup');
const registryPreview = document.getElementById('registryPreview');
const registrySource = document.getElementById('registrySource');
const registryData = document.getElementById('registryData');
btnLookup.addEventListener('click', async function() {
const nip = nipInput.value.replace(/[\s-]/g, '');
if (nip.length !== 10 || !/^\d+$/.test(nip)) {
alert('NIP musi mieć 10 cyfr');
return;
}
btnLookup.disabled = true;
btnLookup.innerHTML = '<span class="loading-spinner"></span> Sprawdzam...';
try {
const response = await fetch('/api/membership/lookup-nip', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nip: nip })
});
const result = await response.json();
if (result.success && result.data) {
registrySource.textContent = result.source;
registryPreview.classList.add('success');
registryPreview.style.display = 'block';
const data = result.data;
registryData.innerHTML = `
<div class="registry-data-row"><span class="registry-data-label">Nazwa:</span> ${data.name || '-'}</div>
<div class="registry-data-row"><span class="registry-data-label">Adres:</span> ${data.address_postal_code || ''} ${data.address_city || ''}, ${data.address_street || ''} ${data.address_number || ''}</div>
${data.krs ? `<div class="registry-data-row"><span class="registry-data-label">KRS:</span> ${data.krs}</div>` : ''}
${data.regon ? `<div class="registry-data-row"><span class="registry-data-label">REGON:</span> ${data.regon}</div>` : ''}
`;
// Auto-fill form
if (data.name) document.querySelector('[name="company_name"]').value = data.name;
if (data.address_postal_code) document.querySelector('[name="address_postal_code"]').value = data.address_postal_code;
if (data.address_city) document.querySelector('[name="address_city"]').value = data.address_city;
if (data.address_street) document.querySelector('[name="address_street"]').value = data.address_street;
if (data.address_number) document.querySelector('[name="address_number"]').value = data.address_number;
if (data.krs) document.querySelector('[name="krs_number"]').value = data.krs;
if (data.regon) document.querySelector('[name="regon"]').value = data.regon;
document.querySelector('[name="registry_source"]').value = result.source;
} else {
registryPreview.classList.remove('success');
registryPreview.style.display = 'block';
registryData.innerHTML = '<p>Firma nie została znaleziona w rejestrze. Wypełnij dane ręcznie.</p>';
document.querySelector('[name="registry_source"]').value = 'manual';
}
} catch (error) {
console.error('Lookup error:', error);
alert('Błąd podczas sprawdzania NIP');
} finally {
btnLookup.disabled = false;
btnLookup.innerHTML = 'Sprawdź w rejestrze';
}
});
{% endif %}
{% if step == 3 %}
document.getElementById('declarationAccepted').addEventListener('change', function() {
const box = document.getElementById('declarationBox');
if (this.checked) {
box.classList.add('accepted');
} else {
box.classList.remove('accepted');
}
});
{% endif %}
{% endblock %}