- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner" - Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash - Zachowano historyczne odniesienia w timeline i dokumentacji Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
628 lines
18 KiB
HTML
Executable File
628 lines
18 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Rejestracja - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block container_class %}container-narrow{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.auth-container {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
padding: var(--spacing-2xl) 0;
|
|
}
|
|
|
|
.auth-card {
|
|
background-color: var(--surface);
|
|
padding: var(--spacing-2xl);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.auth-header {
|
|
text-align: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.auth-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.auth-header p {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-label .required {
|
|
color: var(--error);
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
font-family: var(--font-family);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.form-input.error {
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.form-help {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.password-strength {
|
|
margin-top: var(--spacing-sm);
|
|
height: 4px;
|
|
background-color: var(--border);
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.password-strength-bar {
|
|
height: 100%;
|
|
width: 0;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.password-strength-bar.weak {
|
|
background-color: var(--error);
|
|
width: 33%;
|
|
}
|
|
|
|
.password-strength-bar.medium {
|
|
background-color: var(--warning);
|
|
width: 66%;
|
|
}
|
|
|
|
.password-strength-bar.strong {
|
|
background-color: var(--success);
|
|
width: 100%;
|
|
}
|
|
|
|
.password-requirements {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.password-requirements ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: var(--spacing-sm) 0 0 0;
|
|
}
|
|
|
|
.password-requirements li {
|
|
padding: var(--spacing-xs) 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.password-requirements li .checkbox-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid var(--border);
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--surface);
|
|
transition: all 0.2s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.password-requirements li.valid .checkbox-icon {
|
|
background: var(--success);
|
|
border-color: var(--success);
|
|
}
|
|
|
|
.password-requirements li.valid .checkbox-icon::after {
|
|
content: "✓";
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.password-requirements li.valid {
|
|
color: var(--success);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.form-actions {
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.btn-full {
|
|
width: 100%;
|
|
}
|
|
|
|
.auth-footer {
|
|
text-align: center;
|
|
margin-top: var(--spacing-lg);
|
|
padding-top: var(--spacing-lg);
|
|
border-top: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.auth-footer a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.auth-footer a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.nip-status {
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.nip-status.norda-member {
|
|
background-color: #d1fae5;
|
|
border: 1px solid #10b981;
|
|
color: #065f46;
|
|
}
|
|
|
|
.nip-status.non-member {
|
|
background-color: #dbeafe;
|
|
border: 1px solid #3b82f6;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.nip-status.error {
|
|
background-color: #fee2e2;
|
|
border: 1px solid #ef4444;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.nip-status .icon {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.nip-status.loading {
|
|
background-color: #f3f4f6;
|
|
border: 1px solid #d1d5db;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: var(--surface);
|
|
color: var(--text-primary);
|
|
border: 2px solid var(--border);
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
border-radius: var(--radius);
|
|
font-weight: 500;
|
|
transition: var(--transition);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: var(--background);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.btn-secondary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.email-status {
|
|
font-size: var(--font-size-sm);
|
|
padding: var(--spacing-xs) 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.email-status.available {
|
|
color: var(--success);
|
|
}
|
|
|
|
.email-status.taken {
|
|
color: var(--error);
|
|
}
|
|
|
|
.email-status.checking {
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="auth-container">
|
|
<div class="auth-card">
|
|
<div class="auth-header">
|
|
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes" height="56" style="margin-bottom: var(--spacing-md);">
|
|
<h1>Utwórz konto</h1>
|
|
<p>Dołącz do społeczności Norda Biznes</p>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ url_for('register') }}" novalidate>
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<div class="form-group">
|
|
<label for="name" class="form-label">
|
|
Imię i nazwisko <span class="required">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
class="form-input"
|
|
placeholder="Jan Kowalski"
|
|
required
|
|
autocomplete="name"
|
|
autofocus
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="email" class="form-label">
|
|
Adres email <span class="required">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
class="form-input"
|
|
placeholder="twoj@email.com"
|
|
required
|
|
autocomplete="email"
|
|
>
|
|
<div id="emailStatus" class="form-help" style="display: none; margin-top: var(--spacing-xs);"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="company_nip" class="form-label">
|
|
NIP firmy <span class="required">*</span>
|
|
</label>
|
|
<div style="display: flex; gap: var(--spacing-sm);">
|
|
<input
|
|
type="text"
|
|
id="company_nip"
|
|
name="company_nip"
|
|
class="form-input"
|
|
placeholder="0000000000"
|
|
maxlength="10"
|
|
required
|
|
style="flex: 1;"
|
|
>
|
|
<button
|
|
type="button"
|
|
id="verifyNipBtn"
|
|
class="btn btn-secondary"
|
|
style="white-space: nowrap;"
|
|
>
|
|
Sprawdź NIP
|
|
</button>
|
|
</div>
|
|
<div class="form-help">
|
|
Podaj 10 cyfr bez spacji i myślników (np. 5882465814)
|
|
</div>
|
|
<div id="nipStatus" class="nip-status" style="display: none; margin-top: var(--spacing-sm);"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password" class="form-label">
|
|
Hasło <span class="required">*</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
name="password"
|
|
class="form-input"
|
|
placeholder="••••••••"
|
|
required
|
|
autocomplete="new-password"
|
|
>
|
|
<div class="password-strength">
|
|
<div class="password-strength-bar" id="strengthBar"></div>
|
|
</div>
|
|
<div class="password-requirements">
|
|
<ul id="requirements">
|
|
<li id="req-length">
|
|
<span class="checkbox-icon"></span>
|
|
<span class="requirement-text">Minimum 8 znaków</span>
|
|
</li>
|
|
<li id="req-upper">
|
|
<span class="checkbox-icon"></span>
|
|
<span class="requirement-text">Wielka litera</span>
|
|
</li>
|
|
<li id="req-lower">
|
|
<span class="checkbox-icon"></span>
|
|
<span class="requirement-text">Mała litera</span>
|
|
</li>
|
|
<li id="req-digit">
|
|
<span class="checkbox-icon"></span>
|
|
<span class="requirement-text">Cyfra</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary btn-lg btn-full" id="submitBtn" disabled>
|
|
Zarejestruj się
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="auth-footer">
|
|
<p>Masz już konto? <a href="{{ url_for('login') }}">Zaloguj się</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// Version: 2025-11-24 14:00 - Live checkbox validation
|
|
console.log('🔧 Password validation loaded - Version 2025-11-24 14:00');
|
|
|
|
const passwordInput = document.getElementById('password');
|
|
const strengthBar = document.getElementById('strengthBar');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
// Password strength checker
|
|
passwordInput.addEventListener('input', function() {
|
|
const password = this.value;
|
|
let strength = 0;
|
|
let validCount = 0;
|
|
|
|
// Check requirements
|
|
const hasLength = password.length >= 8;
|
|
const hasUpper = /[A-Z]/.test(password);
|
|
const hasLower = /[a-z]/.test(password);
|
|
const hasDigit = /\d/.test(password);
|
|
|
|
// Update UI for each requirement
|
|
updateRequirement('req-length', hasLength);
|
|
updateRequirement('req-upper', hasUpper);
|
|
updateRequirement('req-lower', hasLower);
|
|
updateRequirement('req-digit', hasDigit);
|
|
|
|
// Calculate strength
|
|
if (hasLength) { strength++; validCount++; }
|
|
if (hasUpper) { strength++; validCount++; }
|
|
if (hasLower) { strength++; validCount++; }
|
|
if (hasDigit) { strength++; validCount++; }
|
|
|
|
// Update strength bar
|
|
strengthBar.className = 'password-strength-bar';
|
|
if (strength === 1 || strength === 2) {
|
|
strengthBar.classList.add('weak');
|
|
} else if (strength === 3) {
|
|
strengthBar.classList.add('medium');
|
|
} else if (strength === 4) {
|
|
strengthBar.classList.add('strong');
|
|
}
|
|
|
|
// Enable submit button only if all requirements met
|
|
submitBtn.disabled = validCount < 4;
|
|
});
|
|
|
|
function updateRequirement(id, valid) {
|
|
const el = document.getElementById(id);
|
|
console.log(`Updating ${id}: ${valid ? 'VALID' : 'invalid'}`); // DEBUG
|
|
if (valid) {
|
|
el.classList.add('valid');
|
|
} else {
|
|
el.classList.remove('valid');
|
|
}
|
|
}
|
|
|
|
// Form validation
|
|
document.querySelector('form').addEventListener('submit', function(e) {
|
|
const name = document.getElementById('name');
|
|
const email = document.getElementById('email');
|
|
const password = document.getElementById('password');
|
|
let valid = true;
|
|
|
|
// Name validation
|
|
if (!name.value || name.value.length < 2) {
|
|
name.classList.add('error');
|
|
valid = false;
|
|
} else {
|
|
name.classList.remove('error');
|
|
}
|
|
|
|
// Email validation
|
|
if (!email.value || !email.value.includes('@')) {
|
|
email.classList.add('error');
|
|
valid = false;
|
|
} else {
|
|
email.classList.remove('error');
|
|
}
|
|
|
|
// Password validation
|
|
const hasLength = password.value.length >= 8;
|
|
const hasUpper = /[A-Z]/.test(password.value);
|
|
const hasLower = /[a-z]/.test(password.value);
|
|
const hasDigit = /\d/.test(password.value);
|
|
|
|
if (!hasLength || !hasUpper || !hasLower || !hasDigit) {
|
|
password.classList.add('error');
|
|
valid = false;
|
|
} else {
|
|
password.classList.remove('error');
|
|
}
|
|
|
|
if (!valid) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Email validation and availability check
|
|
const emailInput = document.getElementById('email');
|
|
const emailStatus = document.getElementById('emailStatus');
|
|
let emailCheckTimeout;
|
|
let emailAvailable = false;
|
|
|
|
emailInput.addEventListener('input', function() {
|
|
const email = this.value.trim();
|
|
|
|
// Clear previous timeout
|
|
clearTimeout(emailCheckTimeout);
|
|
|
|
// Basic format validation
|
|
if (!email) {
|
|
emailStatus.style.display = 'none';
|
|
emailAvailable = false;
|
|
return;
|
|
}
|
|
|
|
// Proper email format validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
if (!emailRegex.test(email)) {
|
|
showEmailStatus('taken', '❌ Nieprawidłowy format email');
|
|
emailAvailable = false;
|
|
return;
|
|
}
|
|
|
|
// Check availability after 500ms of no typing
|
|
emailCheckTimeout = setTimeout(() => {
|
|
checkEmailAvailability(email);
|
|
}, 500);
|
|
});
|
|
|
|
function checkEmailAvailability(email) {
|
|
showEmailStatus('checking', '⏳ Sprawdzam dostępność...');
|
|
|
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
|
|
fetch('/api/check-email', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ email: email })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.available) {
|
|
showEmailStatus('available', '✅ Email dostępny');
|
|
emailAvailable = true;
|
|
} else {
|
|
showEmailStatus('taken', '❌ Email jest już zarejestrowany');
|
|
emailAvailable = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Email check error:', error);
|
|
emailStatus.style.display = 'none';
|
|
emailAvailable = false;
|
|
});
|
|
}
|
|
|
|
function showEmailStatus(statusClass, message) {
|
|
emailStatus.className = 'email-status ' + statusClass;
|
|
emailStatus.textContent = message;
|
|
emailStatus.style.display = 'block';
|
|
}
|
|
|
|
// NIP verification
|
|
const verifyNipBtn = document.getElementById('verifyNipBtn');
|
|
const nipInput = document.getElementById('company_nip');
|
|
const nipStatus = document.getElementById('nipStatus');
|
|
let nipVerified = false;
|
|
let isNordaMember = false;
|
|
|
|
verifyNipBtn.addEventListener('click', function() {
|
|
const nip = nipInput.value.trim();
|
|
|
|
// Validate NIP format (10 digits)
|
|
if (!/^\d{10}$/.test(nip)) {
|
|
showNipStatus('error', '❌ Nieprawidłowy format NIP. Podaj 10 cyfr.');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
showNipStatus('loading', '⏳ Sprawdzam NIP...');
|
|
verifyNipBtn.disabled = true;
|
|
|
|
// Get CSRF token from form
|
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
|
|
// Call API
|
|
fetch('/api/verify-nip', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ nip: nip })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
nipVerified = true;
|
|
isNordaMember = data.is_member;
|
|
|
|
if (data.is_member) {
|
|
showNipStatus('norda-member',
|
|
`✅ ${data.company_name}<br><strong>Firma należy do sieci NORDA</strong> - Konto uprzywilejowane`
|
|
);
|
|
} else {
|
|
showNipStatus('non-member',
|
|
`✅ NIP zweryfikowany<br>Firma spoza sieci NORDA - Konto standardowe`
|
|
);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('NIP verification error:', error);
|
|
showNipStatus('error', '❌ Błąd weryfikacji NIP. Spróbuj ponownie.');
|
|
nipVerified = false;
|
|
})
|
|
.finally(() => {
|
|
verifyNipBtn.disabled = false;
|
|
});
|
|
});
|
|
|
|
function showNipStatus(statusClass, message) {
|
|
nipStatus.className = 'nip-status ' + statusClass;
|
|
nipStatus.innerHTML = '<span class="icon"></span>' + message;
|
|
nipStatus.style.display = 'flex';
|
|
}
|
|
|
|
// Clear status when NIP is modified
|
|
nipInput.addEventListener('input', function() {
|
|
if (nipVerified) {
|
|
nipStatus.style.display = 'none';
|
|
nipVerified = false;
|
|
isNordaMember = false;
|
|
}
|
|
});
|
|
{% endblock %}
|