- Add MembershipFee and MembershipFeeConfig models - Add /health endpoint for monitoring - Add Microsoft Fluent Design CSS - Update templates with new CSS structure - Add Announcement model - Update .gitignore to exclude analysis files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
843 lines
25 KiB
HTML
843 lines
25 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Rejestracja - Norda Biznes Hub{% 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);
|
|
}
|
|
|
|
/* Company autocomplete styles */
|
|
.company-search-container {
|
|
position: relative;
|
|
}
|
|
|
|
.company-search-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);
|
|
}
|
|
|
|
.company-search-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.company-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 250px;
|
|
overflow-y: auto;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
.company-dropdown.show {
|
|
display: block;
|
|
}
|
|
|
|
.company-option {
|
|
padding: var(--spacing-md);
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--border);
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
.company-option:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.company-option:hover {
|
|
background-color: var(--primary-light, #eff6ff);
|
|
}
|
|
|
|
.company-option-name {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.company-option-city {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.company-selected {
|
|
padding: var(--spacing-md);
|
|
background-color: #d1fae5;
|
|
border: 1px solid #10b981;
|
|
border-radius: var(--radius);
|
|
margin-top: var(--spacing-sm);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.company-selected-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.company-selected-name {
|
|
font-weight: 600;
|
|
color: #065f46;
|
|
}
|
|
|
|
.company-selected-nip {
|
|
font-size: var(--font-size-sm);
|
|
color: #047857;
|
|
}
|
|
|
|
.company-selected-badge {
|
|
background: #10b981;
|
|
color: white;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.company-clear-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #065f46;
|
|
cursor: pointer;
|
|
padding: var(--spacing-xs);
|
|
margin-left: var(--spacing-sm);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
.company-clear-btn:hover {
|
|
color: #991b1b;
|
|
}
|
|
|
|
.no-results {
|
|
padding: var(--spacing-md);
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="auth-container">
|
|
<div class="auth-card">
|
|
<div class="auth-header">
|
|
<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_search" class="form-label">
|
|
Firma członkowska Norda Biznes <span class="required">*</span>
|
|
</label>
|
|
<div class="company-search-container">
|
|
<input
|
|
type="text"
|
|
id="company_search"
|
|
class="company-search-input"
|
|
placeholder="Zacznij wpisywać nazwę firmy..."
|
|
autocomplete="off"
|
|
>
|
|
<div id="companyDropdown" class="company-dropdown"></div>
|
|
</div>
|
|
<div class="form-help">
|
|
Wpisz nazwę firmy członkowskiej - lista filtruje się automatycznie
|
|
</div>
|
|
<!-- Selected company display -->
|
|
<div id="companySelected" class="company-selected" style="display: none;">
|
|
<div class="company-selected-info">
|
|
<div class="company-selected-name" id="selectedCompanyName"></div>
|
|
<div class="company-selected-nip">NIP: <span id="selectedCompanyNip"></span></div>
|
|
</div>
|
|
<span class="company-selected-badge">NORDA BIZNES</span>
|
|
<button type="button" class="company-clear-btn" id="clearCompanyBtn" title="Zmień firmę">✕</button>
|
|
</div>
|
|
<!-- Hidden NIP field for form submission -->
|
|
<input type="hidden" id="company_nip" name="company_nip" required>
|
|
</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';
|
|
}
|
|
|
|
// ============================================
|
|
// Company Autocomplete with Real-time Filtering
|
|
// ============================================
|
|
|
|
const companySearch = document.getElementById('company_search');
|
|
const companyDropdown = document.getElementById('companyDropdown');
|
|
const companySelected = document.getElementById('companySelected');
|
|
const selectedCompanyName = document.getElementById('selectedCompanyName');
|
|
const selectedCompanyNip = document.getElementById('selectedCompanyNip');
|
|
const clearCompanyBtn = document.getElementById('clearCompanyBtn');
|
|
const nipHiddenInput = document.getElementById('company_nip');
|
|
|
|
let companies = []; // Will be loaded from API
|
|
let selectedCompany = null;
|
|
|
|
// Load companies on page load
|
|
loadCompanies();
|
|
|
|
async function loadCompanies() {
|
|
try {
|
|
const response = await fetch('/api/companies');
|
|
const data = await response.json();
|
|
companies = data.companies || data;
|
|
console.log(`Loaded ${companies.length} companies for autocomplete`);
|
|
} catch (error) {
|
|
console.error('Failed to load companies:', error);
|
|
companies = [];
|
|
}
|
|
}
|
|
|
|
// Filter and show dropdown on input
|
|
companySearch.addEventListener('input', function() {
|
|
const query = this.value.trim().toLowerCase();
|
|
|
|
if (query.length === 0) {
|
|
hideDropdown();
|
|
return;
|
|
}
|
|
|
|
// Filter companies - match anywhere in name
|
|
const filtered = companies.filter(company =>
|
|
company.name.toLowerCase().includes(query)
|
|
).slice(0, 10); // Limit to 10 results
|
|
|
|
showDropdown(filtered, query);
|
|
});
|
|
|
|
// Show dropdown on focus if there's text
|
|
companySearch.addEventListener('focus', function() {
|
|
if (this.value.trim().length > 0 && !selectedCompany) {
|
|
const query = this.value.trim().toLowerCase();
|
|
const filtered = companies.filter(c => c.name.toLowerCase().includes(query)).slice(0, 10);
|
|
showDropdown(filtered, query);
|
|
}
|
|
});
|
|
|
|
function showDropdown(filteredCompanies, query) {
|
|
if (filteredCompanies.length === 0) {
|
|
companyDropdown.innerHTML = '<div class="no-results">Nie znaleziono firmy o nazwie "' + escapeHtml(query) + '"</div>';
|
|
companyDropdown.classList.add('show');
|
|
return;
|
|
}
|
|
|
|
companyDropdown.innerHTML = filteredCompanies.map(company => `
|
|
<div class="company-option" data-nip="${company.nip || ''}" data-name="${escapeHtml(company.name)}">
|
|
<span class="company-option-name">${highlightMatch(company.name, query)}</span>
|
|
<span class="company-option-city">${company.city || ''}</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Add click handlers
|
|
companyDropdown.querySelectorAll('.company-option').forEach(option => {
|
|
option.addEventListener('click', function() {
|
|
selectCompany(this.dataset.name, this.dataset.nip);
|
|
});
|
|
});
|
|
|
|
companyDropdown.classList.add('show');
|
|
}
|
|
|
|
function hideDropdown() {
|
|
companyDropdown.classList.remove('show');
|
|
}
|
|
|
|
function selectCompany(name, nip) {
|
|
selectedCompany = { name, nip };
|
|
|
|
// Update hidden NIP field
|
|
nipHiddenInput.value = nip;
|
|
|
|
// Show selected company card
|
|
selectedCompanyName.textContent = name;
|
|
selectedCompanyNip.textContent = nip;
|
|
companySelected.style.display = 'flex';
|
|
|
|
// Hide search input and dropdown
|
|
companySearch.style.display = 'none';
|
|
hideDropdown();
|
|
|
|
console.log(`Selected company: ${name} (NIP: ${nip})`);
|
|
}
|
|
|
|
// Clear selection
|
|
clearCompanyBtn.addEventListener('click', function() {
|
|
selectedCompany = null;
|
|
nipHiddenInput.value = '';
|
|
companySelected.style.display = 'none';
|
|
companySearch.style.display = 'block';
|
|
companySearch.value = '';
|
|
companySearch.focus();
|
|
});
|
|
|
|
// Hide dropdown when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!companySearch.contains(e.target) && !companyDropdown.contains(e.target)) {
|
|
hideDropdown();
|
|
}
|
|
});
|
|
|
|
// Keyboard navigation
|
|
companySearch.addEventListener('keydown', function(e) {
|
|
const options = companyDropdown.querySelectorAll('.company-option');
|
|
const active = companyDropdown.querySelector('.company-option:hover, .company-option.active');
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (options.length > 0) {
|
|
const next = active ? active.nextElementSibling || options[0] : options[0];
|
|
options.forEach(o => o.classList.remove('active'));
|
|
next.classList.add('active');
|
|
next.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (options.length > 0) {
|
|
const prev = active ? active.previousElementSibling || options[options.length - 1] : options[options.length - 1];
|
|
options.forEach(o => o.classList.remove('active'));
|
|
prev.classList.add('active');
|
|
prev.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const activeOption = companyDropdown.querySelector('.company-option.active');
|
|
if (activeOption) {
|
|
selectCompany(activeOption.dataset.name, activeOption.dataset.nip);
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
hideDropdown();
|
|
}
|
|
});
|
|
|
|
// Helper functions
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function highlightMatch(text, query) {
|
|
if (!query) return escapeHtml(text);
|
|
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
return escapeHtml(text).replace(regex, '<strong>$1</strong>');
|
|
}
|
|
{% endblock %}
|