{% extends "base.html" %} {% block title %}Zarządzanie Użytkownikami - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Zarządzanie Użytkownikami

Zarządzaj kontami użytkowników platformy

{{ total_users }}
Wszystkich
{{ admin_count }}
Adminów
{{ verified_count }}
Zweryfikowanych
{{ unverified_count }}
Niezweryfikowanych

Użytkownicy

{% if users %} {% for user in users %} {% endfor %}
ID Użytkownik Firma Utworzono Status Akcje
{{ user.id }} {% if user.company %} {{ user.company.name }} {% else %} - {% endif %} {{ user.created_at.strftime('%d.%m.%Y %H:%M') }} {% if user.is_admin %} Admin {% endif %} {% if user.is_verified %} Zweryfikowany {% else %} Niezweryfikowany {% endif %}
{% else %}

Brak użytkowników

{% endif %}
{% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; let currentUserId = null; let confirmCallback = null; let editUserId = null; // Edit User functions function openEditUserModal(userId, name, email, phone) { editUserId = userId; document.getElementById('editUserName').value = name || ''; document.getElementById('editUserEmail').value = email || ''; document.getElementById('editUserPhone').value = phone || ''; document.getElementById('editUserModal').classList.add('active'); } function closeEditUserModal() { editUserId = null; document.getElementById('editUserModal').classList.remove('active'); } async function saveEditUser() { if (!editUserId) return; const name = document.getElementById('editUserName').value.trim(); const email = document.getElementById('editUserEmail').value.trim(); const phone = document.getElementById('editUserPhone').value.trim(); if (!email) { showToast('Email jest wymagany', 'error'); return; } try { const response = await fetch(`/admin/users/${editUserId}/update`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ name: name || null, email: email, phone: phone || null }) }); const data = await response.json(); if (data.success) { closeEditUserModal(); showToast(data.message || 'Zapisano zmiany', 'success'); location.reload(); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } } // Toast notification system function showToast(message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const iconSvg = { success: '', error: '', warning: '' }; toast.innerHTML = ` ${iconSvg[type] || iconSvg.success} ${message} `; container.appendChild(toast); // Auto-remove after 5 seconds setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, 5000); } function showMessage(message, type) { showToast(message, type); } // Confirmation modal system function showConfirmModal(title, description, callback, iconType = 'warning', buttonText = 'Potwierdź', buttonClass = 'btn-primary') { document.getElementById('confirmTitle').textContent = title; document.getElementById('confirmDescription').textContent = description; const icon = document.getElementById('confirmIcon'); icon.className = `modal-icon ${iconType}`; const iconSvg = { warning: '', danger: '', success: '' }; icon.innerHTML = `${iconSvg[iconType] || iconSvg.warning}`; const actionBtn = document.getElementById('confirmAction'); actionBtn.textContent = buttonText; actionBtn.className = `btn ${buttonClass}`; confirmCallback = callback; document.getElementById('confirmModal').classList.add('active'); } function closeConfirmModal() { document.getElementById('confirmModal').classList.remove('active'); confirmCallback = null; } document.getElementById('confirmAction').addEventListener('click', function() { if (confirmCallback) { confirmCallback(); } closeConfirmModal(); }); // Filter tabs functionality document.querySelectorAll('.filter-tab').forEach(tab => { tab.addEventListener('click', function() { document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); const filter = this.dataset.filter; document.querySelectorAll('[data-user-id]').forEach(row => { const isAdmin = row.dataset.isAdmin === 'true'; const isVerified = row.dataset.isVerified === 'true'; let show = false; if (filter === 'all') show = true; else if (filter === 'admin') show = isAdmin; else if (filter === 'verified') show = isVerified; else if (filter === 'unverified') show = !isVerified; row.style.display = show ? '' : 'none'; }); }); }); async function toggleAdmin(userId) { try { const response = await fetch(`/admin/users/${userId}/toggle-admin`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { location.reload(); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showMessage('Błąd połączenia', 'error'); } } async function toggleVerified(userId) { try { const response = await fetch(`/admin/users/${userId}/toggle-verified`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { location.reload(); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showMessage('Błąd połączenia', 'error'); } } // Add User functions function openAddUserModal() { // Reset form document.getElementById('addUserEmail').value = ''; document.getElementById('addUserName').value = ''; document.getElementById('addUserCompany').value = ''; document.getElementById('addUserAdmin').checked = false; document.getElementById('addUserVerified').checked = true; document.getElementById('addUserModal').classList.add('active'); } function closeAddUserModal() { document.getElementById('addUserModal').classList.remove('active'); } async function confirmAddUser() { const email = document.getElementById('addUserEmail').value.trim(); const name = document.getElementById('addUserName').value.trim(); const companyId = document.getElementById('addUserCompany').value || null; const isAdmin = document.getElementById('addUserAdmin').checked; const isVerified = document.getElementById('addUserVerified').checked; if (!email) { showToast('Email jest wymagany', 'error'); return; } // Basic email validation if (!email.includes('@') || !email.includes('.')) { showToast('Podaj poprawny adres email', 'error'); return; } try { const response = await fetch('/admin/users/add', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ email: email, name: name || null, company_id: companyId ? parseInt(companyId) : null, is_admin: isAdmin, is_verified: isVerified }) }); const data = await response.json(); if (data.success) { closeAddUserModal(); // Show the created user modal with password document.getElementById('newUserCreatedEmail').textContent = `Utworzono konto: ${email}`; document.getElementById('newUserPassword').textContent = data.generated_password; document.getElementById('copyPasswordBtnText').textContent = 'Skopiuj hasło'; document.getElementById('newUserCreatedModal').classList.add('active'); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } } function closeNewUserCreatedModal() { document.getElementById('newUserCreatedModal').classList.remove('active'); location.reload(); // Reload to show new user } function copyNewUserPassword() { const password = document.getElementById('newUserPassword').textContent; navigator.clipboard.writeText(password).then(() => { document.getElementById('copyPasswordBtnText').textContent = 'Skopiowano!'; showToast('Hasło skopiowane do schowka', 'success'); setTimeout(() => { document.getElementById('copyPasswordBtnText').textContent = 'Skopiuj hasło'; }, 2000); }).catch(() => { showToast('Nie udało się skopiować', 'error'); }); } function openCompanyModal(userId, userName, currentCompanyId) { currentUserId = userId; document.getElementById('companyModalUser').textContent = `Użytkownik: ${userName}`; document.getElementById('companySelect').value = currentCompanyId || ''; document.getElementById('companyModal').classList.add('active'); } function closeCompanyModal() { currentUserId = null; document.getElementById('companyModal').classList.remove('active'); } async function confirmAssignCompany() { if (!currentUserId) return; const companyId = document.getElementById('companySelect').value || null; try { const response = await fetch(`/admin/users/${currentUserId}/assign-company`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ company_id: companyId ? parseInt(companyId) : null }) }); const data = await response.json(); if (data.success) { closeCompanyModal(); location.reload(); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showMessage('Błąd połączenia', 'error'); } } function resetPassword(userId, userEmail) { showConfirmModal( 'Resetuj hasło', `Czy chcesz wygenerować link do resetu hasła dla ${userEmail}?`, async () => { try { const response = await fetch(`/admin/users/${userId}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { document.getElementById('resetUrl').textContent = data.reset_url; document.getElementById('copyBtnText').textContent = 'Skopiuj link'; document.getElementById('resetModal').classList.add('active'); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } }, 'warning', 'Generuj link', 'btn-primary' ); } function closeResetModal() { document.getElementById('resetModal').classList.remove('active'); } function copyResetUrl() { const url = document.getElementById('resetUrl').textContent; navigator.clipboard.writeText(url).then(() => { document.getElementById('copyBtnText').textContent = 'Skopiowano!'; showToast('Link skopiowany do schowka', 'success'); setTimeout(() => { document.getElementById('copyBtnText').textContent = 'Skopiuj link'; }, 2000); }).catch(() => { showToast('Nie udało się skopiować', 'error'); }); } function deleteUser(userId, userEmail) { showConfirmModal( 'Usuń użytkownika', `Czy na pewno chcesz usunąć użytkownika ${userEmail}? Ta operacja jest nieodwracalna!`, async () => { try { const response = await fetch(`/admin/users/${userId}/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { document.querySelector(`tr[data-user-id="${userId}"]`).remove(); showToast(data.message, 'success'); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } }, 'danger', 'Usuń', 'btn-danger' ); } // Close modals on background click document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', function(e) { if (e.target === this) { this.classList.remove('active'); } }); }); // ======================================== // AI USER CREATION // ======================================== // AI State const AIState = { INPUT: 'input', PROCESSING: 'processing', REVIEW: 'review', CREATING: 'creating', RESULTS: 'results' }; let aiCurrentState = AIState.INPUT; let aiInputType = 'text'; // 'text' or 'image' let aiSelectedFile = null; let aiProposedUsers = []; let aiCreationResults = []; // Open/Close AI Modal function openAIUserModal() { resetAIState(); document.getElementById('aiUserModal').classList.add('active'); } function closeAIUserModal() { document.getElementById('aiUserModal').classList.remove('active'); resetAIState(); } function resetAIState() { aiCurrentState = AIState.INPUT; aiInputType = 'text'; aiSelectedFile = null; aiProposedUsers = []; aiCreationResults = []; // Reset UI document.getElementById('aiUserText').value = ''; removeAIFile(); switchAIInputTab('text'); updateAISteps(1); showAIStep('input'); document.getElementById('aiBackBtn').style.display = 'none'; document.getElementById('aiNextBtn').textContent = 'Analizuj z AI'; document.getElementById('aiNextBtn').disabled = false; } // Tab switching function switchAIInputTab(type, event) { aiInputType = type; document.querySelectorAll('.ai-input-tab').forEach(tab => { tab.classList.remove('active'); }); if (event && event.target) { event.target.closest('.ai-input-tab').classList.add('active'); } else { // Fallback for reset document.querySelector(`.ai-input-tab:${type === 'text' ? 'first' : 'last'}-child`).classList.add('active'); } if (type === 'text') { document.getElementById('aiTextInput').style.display = 'block'; document.getElementById('aiImageInput').style.display = 'none'; } else { document.getElementById('aiTextInput').style.display = 'none'; document.getElementById('aiImageInput').style.display = 'block'; } } // File handling function handleAIFileSelect(event) { const file = event.target.files[0]; if (file) { if (file.size > 5 * 1024 * 1024) { showToast('Plik jest za duży (max 5MB)', 'error'); return; } if (!file.type.startsWith('image/')) { showToast('Dozwolone tylko obrazy', 'error'); return; } aiSelectedFile = file; displayFilePreview(file); } } function displayFilePreview(file) { const reader = new FileReader(); reader.onload = function(e) { document.getElementById('aiPreviewImg').src = e.target.result; document.getElementById('aiFileName').textContent = file.name; document.getElementById('aiFileSize').textContent = formatFileSize(file.size); document.getElementById('aiFilePreview').style.display = 'flex'; document.getElementById('aiFileDropzone').style.display = 'none'; }; reader.readAsDataURL(file); } function removeAIFile() { aiSelectedFile = null; document.getElementById('aiFileInput').value = ''; document.getElementById('aiPreviewImg').src = ''; document.getElementById('aiFilePreview').style.display = 'none'; document.getElementById('aiFileDropzone').style.display = 'block'; } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // Drag and drop const dropzone = document.getElementById('aiFileDropzone'); if (dropzone) { dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); }); dropzone.addEventListener('dragleave', () => { dropzone.classList.remove('drag-over'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); const file = e.dataTransfer.files[0]; if (file) { document.getElementById('aiFileInput').files = e.dataTransfer.files; handleAIFileSelect({ target: { files: [file] } }); } }); } // Clipboard paste support (Ctrl+V / Cmd+V) document.addEventListener('paste', (e) => { // Only handle paste when AI modal is open const modal = document.getElementById('aiUserModal'); if (!modal.classList.contains('active')) return; const items = e.clipboardData?.items; if (!items) return; for (let i = 0; i < items.length; i++) { if (items[i].type.startsWith('image/')) { e.preventDefault(); const file = items[i].getAsFile(); if (file) { // Auto-switch to Screenshot tab aiInputType = 'image'; document.querySelectorAll('.ai-input-tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.ai-input-tab')[1].classList.add('active'); document.getElementById('aiTextInput').style.display = 'none'; document.getElementById('aiImageInput').style.display = 'block'; // Process the pasted image aiSelectedFile = file; displayFilePreview(file); showToast('Obraz wklejony ze schowka', 'success'); } break; } } }); // Steps UI function updateAISteps(step) { for (let i = 1; i <= 3; i++) { const stepEl = document.getElementById(`aiStep${i}`); stepEl.classList.remove('active', 'completed'); if (i < step) { stepEl.classList.add('completed'); } else if (i === step) { stepEl.classList.add('active'); } } } function showAIStep(step) { document.getElementById('aiStepInput').style.display = step === 'input' ? 'block' : 'none'; document.getElementById('aiStepReview').style.display = step === 'review' ? 'block' : 'none'; document.getElementById('aiStepResults').style.display = step === 'results' ? 'block' : 'none'; } // Navigation function aiGoBack() { if (aiCurrentState === AIState.REVIEW) { aiCurrentState = AIState.INPUT; updateAISteps(1); showAIStep('input'); document.getElementById('aiBackBtn').style.display = 'none'; document.getElementById('aiNextBtn').textContent = 'Analizuj z AI'; } } async function aiGoNext() { if (aiCurrentState === AIState.INPUT) { // Validate input if (aiInputType === 'text') { const text = document.getElementById('aiUserText').value.trim(); if (!text) { showToast('Wklej dane użytkowników', 'error'); return; } } else { if (!aiSelectedFile) { showToast('Wybierz screenshot z danymi', 'error'); return; } } // Move to processing aiCurrentState = AIState.PROCESSING; updateAISteps(2); showAIStep('review'); document.getElementById('aiLoading').style.display = 'flex'; document.getElementById('aiResponse').style.display = 'none'; document.getElementById('aiBackBtn').style.display = 'none'; document.getElementById('aiNextBtn').textContent = 'Przetwarzanie...'; document.getElementById('aiNextBtn').disabled = true; await processWithAI(); } else if (aiCurrentState === AIState.REVIEW) { // Get selected users const selectedUsers = getSelectedUsers(); if (selectedUsers.length === 0) { showToast('Wybierz co najmniej jednego użytkownika', 'error'); return; } // Move to creating aiCurrentState = AIState.CREATING; document.getElementById('aiNextBtn').textContent = 'Tworzenie...'; document.getElementById('aiNextBtn').disabled = true; await createUsers(selectedUsers); } else if (aiCurrentState === AIState.RESULTS) { closeAIUserModal(); location.reload(); } } // AI Processing async function processWithAI() { try { let response; if (aiInputType === 'text') { const text = document.getElementById('aiUserText').value.trim(); response = await fetch('/api/admin/users/ai-parse', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ input_type: 'text', content: text }) }); } else { const formData = new FormData(); formData.append('input_type', 'image'); formData.append('file', aiSelectedFile); response = await fetch('/api/admin/users/ai-parse', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }, body: formData }); } const data = await response.json(); document.getElementById('aiLoading').style.display = 'none'; document.getElementById('aiResponse').style.display = 'block'; if (data.success) { aiProposedUsers = data.proposed_users || []; document.getElementById('aiAnalysisText').textContent = data.ai_response || 'Analiza zakończona.'; // Show duplicates warning if (data.duplicate_emails && data.duplicate_emails.length > 0) { document.getElementById('aiDuplicatesWarning').style.display = 'block'; document.getElementById('aiDuplicatesText').textContent = `Te emaile już istnieją w systemie: ${data.duplicate_emails.join(', ')}`; } else { document.getElementById('aiDuplicatesWarning').style.display = 'none'; } // Render users table renderUsersTable(aiProposedUsers, data.duplicate_emails || []); aiCurrentState = AIState.REVIEW; document.getElementById('aiBackBtn').style.display = 'block'; document.getElementById('aiNextBtn').textContent = 'Utwórz konta'; document.getElementById('aiNextBtn').disabled = false; } else { document.getElementById('aiAnalysisText').textContent = data.error || 'Wystąpił błąd podczas analizy.'; document.getElementById('aiBackBtn').style.display = 'block'; document.getElementById('aiNextBtn').textContent = 'Spróbuj ponownie'; document.getElementById('aiNextBtn').disabled = false; aiCurrentState = AIState.INPUT; } } catch (error) { console.error('AI processing error:', error); document.getElementById('aiLoading').style.display = 'none'; document.getElementById('aiResponse').style.display = 'block'; document.getElementById('aiAnalysisText').textContent = 'Błąd połączenia z serwerem. Spróbuj ponownie.'; document.getElementById('aiBackBtn').style.display = 'block'; document.getElementById('aiNextBtn').textContent = 'Spróbuj ponownie'; document.getElementById('aiNextBtn').disabled = false; aiCurrentState = AIState.INPUT; } } function renderUsersTable(users, duplicates) { const tbody = document.getElementById('aiUsersTableBody'); tbody.innerHTML = ''; if (users.length === 0) { tbody.innerHTML = 'Nie znaleziono użytkowników'; return; } users.forEach((user, index) => { const isDuplicate = duplicates.includes(user.email); const hasWarnings = user.warnings && user.warnings.length > 0; const tr = document.createElement('tr'); tr.innerHTML = ` ${user.email} ${isDuplicate ? 'Duplikat' : ''} ${user.name || '-'} ${user.company_name || '-'} ${user.is_admin ? 'Tak' : 'Nie'} ${hasWarnings ? `
${user.warnings.join(', ')}
` : ''} `; tbody.appendChild(tr); }); } function getSelectedUsers() { const selected = []; document.querySelectorAll('.ai-user-checkbox:checked').forEach(cb => { const index = parseInt(cb.dataset.index); if (aiProposedUsers[index]) { selected.push(aiProposedUsers[index]); } }); return selected; } // Create Users async function createUsers(users) { try { const response = await fetch('/api/admin/users/bulk-create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ users: users }) }); const data = await response.json(); aiCurrentState = AIState.RESULTS; updateAISteps(3); showAIStep('results'); if (data.success) { aiCreationResults = { created: data.created || [], failed: data.failed || [] }; const createdCount = aiCreationResults.created.length; const failedCount = aiCreationResults.failed.length; document.getElementById('aiResultsSummary').textContent = `Utworzono ${createdCount} ${createdCount === 1 ? 'konto' : 'kont'}` + (failedCount > 0 ? `, ${failedCount} nie udało się utworzyć.` : '.'); renderResults(aiCreationResults); document.getElementById('aiBackBtn').style.display = 'none'; document.getElementById('aiNextBtn').textContent = 'Zamknij'; document.getElementById('aiNextBtn').disabled = false; } else { document.getElementById('aiResultsSummary').textContent = data.error || 'Wystąpił błąd podczas tworzenia kont.'; document.getElementById('aiResultsList').innerHTML = ''; document.getElementById('aiBackBtn').style.display = 'block'; document.getElementById('aiNextBtn').textContent = 'Zamknij'; document.getElementById('aiNextBtn').disabled = false; } } catch (error) { console.error('Create users error:', error); aiCurrentState = AIState.RESULTS; updateAISteps(3); showAIStep('results'); document.getElementById('aiResultsSummary').textContent = 'Błąd połączenia z serwerem.'; document.getElementById('aiResultsList').innerHTML = ''; document.getElementById('aiBackBtn').style.display = 'block'; document.getElementById('aiNextBtn').textContent = 'Zamknij'; document.getElementById('aiNextBtn').disabled = false; } } function renderResults(results) { const container = document.getElementById('aiResultsList'); container.innerHTML = ''; // Created users results.created.forEach(user => { const div = document.createElement('div'); div.className = 'ai-result-item success'; div.innerHTML = `
${user.email} ${user.generated_password}
`; container.appendChild(div); }); // Failed users results.failed.forEach(user => { const div = document.createElement('div'); div.className = 'ai-result-item error'; div.innerHTML = `
${user.email} ${user.error}
`; container.appendChild(div); }); } function copyPassword(password, button) { navigator.clipboard.writeText(password).then(() => { button.textContent = 'Skopiowano!'; setTimeout(() => { button.textContent = 'Kopiuj hasło'; }, 2000); }).catch(() => { showToast('Nie udało się skopiować', 'error'); }); } {% endblock %}