nordabiz/templates/admin/users.html
Maciej Pienczyn 40ee5db139
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: v3 email template + horizontal action buttons grid in /admin/users
Email: dark header with compass, company card, green checkmarks, Polish
date format, full footer with address, phone and tech support contact.
Actions: 4-column grid layout instead of vertical stack.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:11:04 +01:00

2828 lines
100 KiB
HTML

{% extends "base.html" %}
{% block title %}Zarządzanie Użytkownikami - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.admin-header-content h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin: 0;
}
.admin-header-content p {
margin: var(--spacing-xs) 0 0 0;
}
.btn-add-user {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.btn-add-user:hover {
opacity: 0.9;
}
.btn-add-user svg {
width: 20px;
height: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
text-align: center;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: 700;
color: var(--primary);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.filter-tabs {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
.filter-tab {
padding: var(--spacing-sm) var(--spacing-md);
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: var(--font-size-base);
font-weight: 500;
transition: var(--transition);
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.filter-tab:hover {
color: var(--text-primary);
}
.filter-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
.users-table {
width: 100%;
border-collapse: collapse;
}
.users-table th,
.users-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.users-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.users-table tr:hover {
background: var(--background);
}
.user-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.user-name {
font-weight: 500;
color: var(--text-primary);
max-width: none;
overflow: visible;
text-overflow: unset;
white-space: normal;
}
.user-email {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.user-company {
font-size: var(--font-size-sm);
color: var(--primary);
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
.badge-admin {
background: #DBEAFE;
color: #1D4ED8;
}
.badge-rada {
background: #FEF3C7;
color: #D97706;
}
.role-select {
padding: 4px 8px;
font-size: var(--font-size-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
cursor: pointer;
min-width: 140px;
}
.role-select:hover:not(:disabled) {
border-color: var(--primary);
}
.role-select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.role-select option {
padding: 4px;
}
.badge-verified {
background: #D1FAE5;
color: #065F46;
}
.badge-unverified {
background: #FEF3C7;
color: #92400E;
}
.action-buttons {
display: grid;
grid-template-columns: repeat(4, 32px);
gap: 4px;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: var(--transition);
position: relative;
}
.btn-icon:hover {
background: var(--background);
}
.btn-icon[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: #1F2937;
color: white;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 100;
}
.btn-icon[data-tooltip]:hover::after {
opacity: 1;
}
.btn-icon.admin-toggle {
background: #DBEAFE;
border-color: #3B82F6;
color: #1D4ED8;
}
.btn-icon.admin-toggle.active {
background: #3B82F6;
color: white;
}
.btn-icon.verify-toggle {
background: #D1FAE5;
border-color: #10B981;
color: #065F46;
}
.btn-icon.verify-toggle.active {
background: #10B981;
color: white;
}
.btn-icon.danger:hover {
background: var(--error);
border-color: var(--error);
color: white;
}
.btn-icon svg {
width: 16px;
height: 16px;
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 500px;
width: 90%;
box-shadow: var(--shadow-lg);
}
.modal-header {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
.modal-body {
margin-bottom: var(--spacing-lg);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-label {
display: block;
margin-bottom: var(--spacing-xs);
color: var(--text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
}
.form-control {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
}
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
background: var(--background);
color: var(--text-secondary);
}
.btn-secondary:hover {
background: var(--border);
}
.reset-url {
background: var(--background);
padding: var(--spacing-sm);
border-radius: var(--radius);
word-break: break-all;
font-family: monospace;
font-size: var(--font-size-sm);
margin-top: var(--spacing-sm);
}
.copy-btn {
margin-top: var(--spacing-sm);
font-size: var(--font-size-sm);
}
/* Toast Notification System */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--surface);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-width: 280px;
max-width: 400px;
animation: slideIn 0.3s ease;
border-left: 4px solid var(--primary);
}
.toast.success {
border-left-color: #10B981;
}
.toast.error {
border-left-color: #EF4444;
}
.toast.warning {
border-left-color: #F59E0B;
}
.toast-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.toast-icon.success { color: #10B981; }
.toast-icon.error { color: #EF4444; }
.toast-icon.warning { color: #F59E0B; }
.toast-message {
flex: 1;
color: var(--text-primary);
}
.toast-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* Confirmation Modal */
.modal-icon {
width: 48px;
height: 48px;
margin: 0 auto var(--spacing-md);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.modal-icon.warning {
background: #FEF3C7;
color: #F59E0B;
}
.modal-icon.danger {
background: #FEE2E2;
color: #EF4444;
}
.modal-icon.success {
background: #D1FAE5;
color: #10B981;
}
.modal-icon svg {
width: 24px;
height: 24px;
}
.modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
text-align: center;
margin-bottom: var(--spacing-sm);
}
.modal-description {
text-align: center;
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
.btn-danger {
background: #EF4444;
color: white;
}
.btn-danger:hover {
background: #DC2626;
}
.btn-success {
background: #10B981;
color: white;
}
.btn-success:hover {
background: #059669;
}
/* Reset URL improved styling */
.reset-url-container {
background: linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%);
border: 1px solid #BAE6FD;
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
margin: var(--spacing-md) 0;
}
.reset-url-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.reset-url {
background: var(--surface);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
word-break: break-all;
font-family: monospace;
font-size: var(--font-size-sm);
border: 1px solid var(--border);
max-height: 100px;
overflow-y: auto;
}
.reset-url-info {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
.reset-url-info svg {
width: 16px;
height: 16px;
color: #F59E0B;
}
.copy-btn-row {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.copy-btn-row .btn {
flex: 1;
}
.copied-indicator {
color: #10B981;
font-weight: 500;
}
@media (max-width: 768px) {
.users-table {
font-size: var(--font-size-sm);
}
.users-table th:nth-child(4),
.users-table td:nth-child(4),
.users-table th:nth-child(5),
.users-table td:nth-child(5),
.users-table th:nth-child(6),
.users-table td:nth-child(6) {
display: none;
}
}
/* AI User Creation Styles */
.btn-add-user-ai {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%);
color: white;
border: none;
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
margin-left: var(--spacing-sm);
}
.btn-add-user-ai:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-add-user-ai svg {
width: 20px;
height: 20px;
}
.header-buttons {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* AI Modal - wider */
#aiUserModal .modal-content {
max-width: 700px;
width: 95%;
max-height: 90vh;
overflow-y: auto;
}
.ai-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.ai-modal-title {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.ai-badge {
background: linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%);
color: white;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
}
.ai-steps {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xl);
}
.ai-step {
display: flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.ai-step.active {
color: var(--primary);
font-weight: 500;
}
.ai-step.completed {
color: #10B981;
}
.ai-step-number {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-weight: 600;
}
.ai-step.active .ai-step-number {
background: var(--primary);
color: white;
}
.ai-step.completed .ai-step-number {
background: #10B981;
color: white;
}
.ai-step-line {
width: 40px;
height: 2px;
background: var(--border);
}
.ai-step.completed + .ai-step-line {
background: #10B981;
}
/* Step 1 - Input */
.ai-input-section {
margin-bottom: var(--spacing-lg);
}
.ai-input-tabs {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.ai-input-tab {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--background);
border: 2px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
text-align: center;
transition: var(--transition);
font-weight: 500;
}
.ai-input-tab:hover {
border-color: var(--primary);
}
.ai-input-tab.active {
background: #EEF2FF;
border-color: var(--primary);
color: var(--primary);
}
.ai-input-tab svg {
width: 20px;
height: 20px;
margin-bottom: var(--spacing-xs);
display: block;
margin-left: auto;
margin-right: auto;
}
.ai-textarea {
width: 100%;
min-height: 150px;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-family: inherit;
font-size: var(--font-size-base);
resize: vertical;
}
.ai-textarea:focus {
outline: none;
border-color: var(--primary);
}
.ai-file-upload {
border: 2px dashed var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-2xl);
text-align: center;
cursor: pointer;
transition: var(--transition);
}
.ai-file-upload:hover {
border-color: var(--primary);
background: #F8FAFC;
}
.ai-file-upload.drag-over {
border-color: var(--primary);
background: #EEF2FF;
}
.ai-file-upload svg {
width: 48px;
height: 48px;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.ai-file-upload p {
color: var(--text-secondary);
margin: 0;
}
.ai-file-preview {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.ai-file-preview img {
max-width: 100px;
max-height: 100px;
border-radius: var(--radius);
object-fit: cover;
}
.ai-file-preview .file-info {
flex: 1;
}
.ai-file-preview .file-name {
font-weight: 500;
margin-bottom: var(--spacing-xs);
}
.ai-file-preview .file-size {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.ai-file-preview .remove-file {
color: var(--error);
cursor: pointer;
padding: var(--spacing-sm);
}
.ai-hint {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
/* Step 2 - Review */
.ai-message {
padding: var(--spacing-md);
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-md);
}
.ai-message.assistant {
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
border-left: 4px solid #6366F1;
}
.ai-message.user {
background: var(--background);
border-left: 4px solid var(--border);
}
.ai-message-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
font-weight: 500;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.ai-message-header svg {
width: 16px;
height: 16px;
}
.ai-users-table {
width: 100%;
border-collapse: collapse;
margin-top: var(--spacing-md);
font-size: var(--font-size-sm);
}
.ai-users-table th,
.ai-users-table td {
padding: var(--spacing-sm);
text-align: left;
border-bottom: 1px solid var(--border);
}
.ai-users-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-xs);
text-transform: uppercase;
}
.ai-users-table tr:hover {
background: rgba(99, 102, 241, 0.05);
}
.ai-user-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.ai-user-warning {
color: #F59E0B;
font-size: var(--font-size-xs);
display: flex;
align-items: center;
gap: 4px;
}
.ai-user-warning svg {
width: 14px;
height: 14px;
}
.ai-duplicate-badge {
background: #FEF3C7;
color: #92400E;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
}
/* Step 3 - Results */
.ai-results-list {
margin-top: var(--spacing-md);
}
.ai-result-item {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md);
border-radius: var(--radius);
margin-bottom: var(--spacing-sm);
background: var(--background);
}
.ai-result-item.success {
background: #D1FAE5;
border-left: 4px solid #10B981;
}
.ai-result-item.error {
background: #FEE2E2;
border-left: 4px solid #EF4444;
}
.ai-result-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.ai-result-icon.success { color: #10B981; }
.ai-result-icon.error { color: #EF4444; }
.ai-result-info {
flex: 1;
}
.ai-result-email {
font-weight: 500;
}
.ai-result-password {
font-family: monospace;
background: rgba(0,0,0,0.05);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
margin-left: var(--spacing-sm);
}
.ai-result-copy {
background: none;
border: 1px solid var(--border);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
cursor: pointer;
font-size: var(--font-size-sm);
transition: var(--transition);
}
.ai-result-copy:hover {
background: var(--surface);
}
/* Loading state */
.ai-loading {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--spacing-2xl);
}
.ai-spinner {
width: 48px;
height: 48px;
border: 4px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.ai-loading-text {
margin-top: var(--spacing-md);
color: var(--text-secondary);
}
/* Modal footer */
.ai-modal-footer {
display: flex;
justify-content: space-between;
margin-top: var(--spacing-xl);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border);
}
.ai-modal-footer .btn {
min-width: 120px;
}
</style>
{% endblock %}
{% block content %}
<div class="admin-header">
<div class="admin-header-content">
<h1>Zarządzanie Użytkownikami</h1>
<p class="text-muted">Zarządzaj kontami użytkowników platformy</p>
</div>
<div class="header-buttons">
<button class="btn-add-user" onclick="openAddUserModal()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/>
</svg>
Dodaj użytkownika
</button>
<button class="btn-add-user-ai" onclick="openAIUserModal()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Dodaj z AI
</button>
</div>
</div>
{% if pending_company_count > 0 %}
<div style="background: #FEF3C7; border: 1px solid #F59E0B; border-radius: var(--radius); padding: var(--spacing-md) var(--spacing-lg); margin-bottom: var(--spacing-lg); display: flex; align-items: center; gap: var(--spacing-md);">
<span style="font-size: 1.3em;">⚠️</span>
<span><strong>{{ pending_company_count }}</strong> {{ 'użytkownik czeka' if pending_company_count == 1 else 'użytkowników czeka' }} na zatwierdzenie uprawnień firmowych (firma przypisana, rola: Brak)</span>
</div>
{% endif %}
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ total_users }}</div>
<div class="stat-label">Wszystkich</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #3B82F6;">{{ admin_count }}</div>
<div class="stat-label">Adminów</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #10B981;">{{ verified_count }}</div>
<div class="stat-label">Zweryfikowanych</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #F59E0B;">{{ unverified_count }}</div>
<div class="stat-label">Niezweryfikowanych</div>
</div>
</div>
<!-- Users Section -->
<div class="section">
<h2>Użytkownicy</h2>
<!-- Filter Tabs -->
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">Wszyscy</button>
<button class="filter-tab" data-filter="admin">Admini ({{ admin_count }})</button>
<button class="filter-tab" data-filter="verified">Zweryfikowani ({{ verified_count }})</button>
<button class="filter-tab" data-filter="unverified">Niezweryfikowani ({{ unverified_count }})</button>
{% if pending_company_count > 0 %}
<button class="filter-tab" data-filter="pending-company" style="color: #D97706;">Oczekujący ({{ pending_company_count }})</button>
{% endif %}
</div>
{% if users %}
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>Użytkownik</th>
<th>Firma</th>
<th>Upr. firmowe</th>
<th>Rola</th>
<th>Utworzono</th>
<th>Ostatnie logowanie</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr data-user-id="{{ user.id }}"
data-role="{{ user.role }}"
data-is-verified="{{ 'true' if user.is_verified else 'false' }}"
data-pending-company="{{ 'true' if user.company_id and user.company_role == 'NONE' else 'false' }}">
<td>{{ user.id }}</td>
<td>
<div class="user-info">
<span class="user-name">{{ user.name or '(brak nazwy)' }}</span>
<span class="user-email">{{ user.email }}</span>
</div>
</td>
<td>
{% if user.company %}
<a href="{{ url_for('company_detail_by_slug', slug=user.company.slug) }}"
class="user-company" target="_blank">
{{ user.company.name }}
</a>
{% else %}
<span style="color: var(--text-secondary);">-</span>
{% endif %}
</td>
<td>
{% if user.company %}
<select class="role-select" style="font-size: var(--font-size-sm);"
data-user-id="{{ user.id }}"
onchange="changeCompanyRole({{ user.id }}, this.value)">
<option value="NONE" {% if user.company_role == 'NONE' %}selected{% endif %}>Brak</option>
<option value="VIEWER" {% if user.company_role == 'VIEWER' %}selected{% endif %}>Podgląd</option>
<option value="EMPLOYEE" {% if user.company_role == 'EMPLOYEE' %}selected{% endif %}>Pracownik</option>
<option value="MANAGER" {% if user.company_role == 'MANAGER' %}selected{% endif %}>Zarządzający</option>
</select>
{% else %}
<span style="color: var(--text-secondary);">-</span>
{% endif %}
</td>
<td>
<select class="role-select"
data-user-id="{{ user.id }}"
onchange="changeUserRole({{ user.id }}, this.value)"
{% if user.id == current_user.id %}disabled title="Nie możesz zmienić swojej roli"{% endif %}>
<option value="UNAFFILIATED" {% if user.role == 'UNAFFILIATED' %}selected{% endif %}>Niezrzeszony</option>
<option value="MEMBER" {% if user.role == 'MEMBER' %}selected{% endif %}>Członek</option>
<option value="EMPLOYEE" {% if user.role == 'EMPLOYEE' %}selected{% endif %}>Pracownik</option>
<option value="MANAGER" {% if user.role == 'MANAGER' %}selected{% endif %}>Kadra Zarządzająca</option>
<option value="OFFICE_MANAGER" {% if user.role == 'OFFICE_MANAGER' %}selected{% endif %}>Kierownik Biura</option>
<option value="ADMIN" {% if user.role == 'ADMIN' %}selected{% endif %}>Administrator</option>
</select>
</td>
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);">
{{ user.created_at.strftime('%d.%m.%Y %H:%M') }}
</td>
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);">
{% if user.last_login %}
{{ user.last_login.strftime('%d.%m.%Y %H:%M') }}
{% else %}
<span style="color: var(--text-muted, #9CA3AF);">Nigdy</span>
{% endif %}
</td>
<td>
{% if user.can_manage_users() %}
<span class="badge badge-admin">Admin</span>
{% endif %}
{% if user.is_rada_member %}
<span class="badge badge-rada">Rada</span>
{% endif %}
{% if user.is_verified %}
<span class="badge badge-verified">Zweryfikowany</span>
{% else %}
<span class="badge badge-unverified">Niezweryfikowany</span>
{% endif %}
</td>
<td>
<div class="action-buttons">
<!-- Edit User -->
<button class="btn-icon"
onclick="openEditUserModal({{ user.id }}, '{{ user.name|e if user.name else '' }}', '{{ user.email|e }}', '{{ user.phone|e if user.phone else '' }}')"
data-tooltip="Edytuj dane">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</button>
<!-- Toggle Admin -->
<button class="btn-icon admin-toggle {{ 'active' if user.can_manage_users() else '' }}"
onclick="toggleAdmin({{ user.id }})"
data-tooltip="{{ 'Odbierz uprawnienia admina' if user.can_manage_users() else 'Nadaj uprawnienia admina' }}"
{% if user.id == current_user.id %}disabled{% endif %}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</button>
<!-- Toggle Verified -->
<button class="btn-icon verify-toggle {{ 'active' if user.is_verified else '' }}"
onclick="toggleVerified({{ user.id }})"
data-tooltip="{{ 'Cofnij weryfikację' if user.is_verified else 'Zweryfikuj użytkownika' }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
<!-- Toggle Rada Member -->
<button class="btn-icon rada-toggle {{ 'active' if user.is_rada_member else '' }}"
onclick="toggleRadaMember({{ user.id }})"
data-tooltip="{{ 'Usuń z Rady Izby' if user.is_rada_member else 'Dodaj do Rady Izby' }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</button>
<!-- Assign Company -->
<button class="btn-icon"
onclick="openCompanyModal({{ user.id }}, '{{ user.name|e }}', {{ user.company_id or 'null' }})"
data-tooltip="Przypisz firmę">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
</button>
<!-- Reset Password -->
<button class="btn-icon"
onclick="openPasswordModal({{ user.id }}, '{{ user.email|e }}')"
data-tooltip="Resetuj hasło">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
</button>
<!-- Send role notification -->
{% if user.company and user.email %}
<button class="btn-icon"
onclick="sendRoleNotification({{ user.id }}, '{{ user.name|e }}', '{{ user.email|e }}')"
data-tooltip="Wyślij powiadomienie o uprawnieniach">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</button>
{% endif %}
<!-- Delete -->
<button class="btn-icon danger"
onclick="deleteUser({{ user.id }}, '{{ user.email|e }}')"
data-tooltip="Usuń użytkownika"
{% if user.id == current_user.id %}disabled{% endif %}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Brak użytkowników</p>
</div>
{% endif %}
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">Dodaj nowego użytkownika</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Email *</label>
<input type="email" id="addUserEmail" class="form-control" placeholder="jan.kowalski@firma.pl" required>
</div>
<div class="form-group">
<label class="form-label">Imię i nazwisko</label>
<input type="text" id="addUserName" class="form-control" placeholder="Jan Kowalski">
</div>
<div class="form-group">
<label class="form-label">Firma</label>
<select id="addUserCompany" class="form-control">
<option value="">-- Brak firmy --</option>
{% for company in companies %}
<option value="{{ company.id }}">{{ company.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="display: flex; gap: var(--spacing-lg); align-items: flex-end;">
<div>
<label>Rola</label>
<select id="addUserRole" class="form-control" style="min-width: 160px;">
<option value="MEMBER">Członek</option>
<option value="EMPLOYEE">Pracownik</option>
<option value="MANAGER">Kadra Zarządzająca</option>
<option value="OFFICE_MANAGER">Kierownik Biura</option>
<option value="ADMIN">Administrator</option>
</select>
</div>
<label style="display: flex; align-items: center; gap: var(--spacing-xs); cursor: pointer;">
<input type="checkbox" id="addUserVerified" checked>
Zweryfikowany
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddUserModal()">Anuluj</button>
<button class="btn btn-primary" onclick="confirmAddUser()">Utwórz użytkownika</button>
</div>
</div>
</div>
<!-- New User Created Modal (show generated password) -->
<div id="newUserCreatedModal" class="modal">
<div class="modal-content">
<div class="modal-icon success">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="modal-title">Użytkownik utworzony</div>
<div class="modal-description" id="newUserCreatedEmail"></div>
<div class="reset-url-container">
<div class="reset-url-label">Wygenerowane hasło (przekaż użytkownikowi):</div>
<div id="newUserPassword" class="reset-url" style="font-weight: bold; font-size: var(--font-size-lg);"></div>
<div class="reset-url-info">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span>Użytkownik powinien zmienić hasło po pierwszym logowaniu</span>
</div>
</div>
<div class="copy-btn-row">
<button class="btn btn-primary" onclick="copyNewUserPassword()">
<span id="copyPasswordBtnText">Skopiuj hasło</span>
</button>
<button class="btn btn-secondary" onclick="closeNewUserCreatedModal()">Zamknij</button>
</div>
</div>
</div>
<!-- Company Assignment Modal -->
<div id="companyModal" class="modal">
<div class="modal-content">
<div class="modal-header">Przypisz firmę</div>
<div class="modal-body">
<p id="companyModalUser" style="margin-bottom: var(--spacing-md);"></p>
<div class="form-group">
<label class="form-label">Wybierz firmę</label>
<select id="companySelect" class="form-control">
<option value="">-- Brak firmy --</option>
{% for company in companies %}
<option value="{{ company.id }}">{{ company.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeCompanyModal()">Anuluj</button>
<button class="btn btn-primary" onclick="confirmAssignCompany()">Zapisz</button>
</div>
</div>
</div>
<!-- Password Management Modal -->
<div id="passwordModal" class="modal">
<div class="modal-content" style="max-width: 520px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg);">
<div class="modal-header" style="margin-bottom: 0;">Resetuj hasło</div>
<button class="btn-icon" onclick="closePasswordModal()" style="border: none;">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-lg);">
Użytkownik: <strong id="pwModalEmail"></strong>
</div>
<!-- Tabs -->
<div style="display: flex; gap: var(--spacing-sm); margin-bottom: var(--spacing-lg);">
<button class="btn btn-primary" id="pwTabSet" onclick="switchPwTab('set')" style="flex: 1;">Ustaw hasło</button>
<button class="btn btn-secondary" id="pwTabLink" onclick="switchPwTab('link')" style="flex: 1;">Wyślij link</button>
</div>
<!-- Tab: Set Password -->
<div id="pwSetPanel">
<div class="form-group">
<label class="form-label">Nowe hasło</label>
<div style="display: flex; gap: var(--spacing-xs);">
<div style="flex: 1; position: relative;">
<input type="password" id="pwNewPassword" class="form-control" placeholder="Min. 8 znaków" autocomplete="new-password">
<button type="button" onclick="togglePwVisibility()" style="position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: var(--text-secondary); padding: 4px;">
<svg id="pwEyeIcon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="18" height="18">
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
</div>
<button class="btn btn-secondary" onclick="generatePassword()" style="white-space: nowrap;">Generuj</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closePasswordModal()">Anuluj</button>
<button class="btn btn-primary" onclick="saveDirectPassword()">Zapisz hasło</button>
</div>
</div>
<!-- Tab: Reset Link -->
<div id="pwLinkPanel" style="display: none;">
<div class="modal-description" style="text-align: left;">
Wygeneruj jednorazowy link, który pozwoli użytkownikowi samodzielnie ustawić nowe hasło. Link jest ważny 1 godzinę.
</div>
<div id="pwLinkResult" style="display: none;">
<div class="reset-url-container">
<div class="reset-url-label">Link do resetu hasła:</div>
<div id="resetUrl" class="reset-url"></div>
<div class="reset-url-info">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Link ważny przez 1 godzinę</span>
</div>
</div>
<div style="margin-top: var(--spacing-md);">
<button class="btn btn-primary" onclick="copyResetUrl()" style="width: 100%;">
<span id="copyBtnText">Skopiuj link</span>
</button>
</div>
</div>
<div class="modal-footer" id="pwLinkActions">
<button class="btn btn-secondary" onclick="closePasswordModal()">Anuluj</button>
<button class="btn btn-primary" onclick="generateResetLink()">Generuj link</button>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div id="editUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">Edytuj użytkownika</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Imię i nazwisko</label>
<input type="text" id="editUserName" class="form-control" placeholder="Jan Kowalski">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" id="editUserEmail" class="form-control" placeholder="jan@firma.pl">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="text" id="editUserPhone" class="form-control" placeholder="+48 123 456 789">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeEditUserModal()">Anuluj</button>
<button class="btn btn-primary" onclick="saveEditUser()">Zapisz zmiany</button>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<div id="confirmIcon" class="modal-icon warning">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div id="confirmTitle" class="modal-title">Potwierdzenie</div>
<div id="confirmDescription" class="modal-description">Czy na pewno chcesz wykonać tę akcję?</div>
<div class="modal-footer" style="justify-content: center;">
<button class="btn btn-secondary" onclick="closeConfirmModal()">Anuluj</button>
<button id="confirmAction" class="btn btn-primary">Potwierdź</button>
</div>
</div>
</div>
<!-- AI User Creation Modal -->
<div id="aiUserModal" class="modal">
<div class="modal-content">
<div class="ai-modal-header">
<div class="ai-modal-title">
Tworzenie użytkowników
<span class="ai-badge">Gemini AI</span>
</div>
<button class="btn-icon" onclick="closeAIUserModal()" data-tooltip="Zamknij">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Steps Indicator -->
<div class="ai-steps">
<div class="ai-step active" id="aiStep1">
<span class="ai-step-number">1</span>
Dane
</div>
<div class="ai-step-line"></div>
<div class="ai-step" id="aiStep2">
<span class="ai-step-number">2</span>
Przegląd
</div>
<div class="ai-step-line"></div>
<div class="ai-step" id="aiStep3">
<span class="ai-step-number">3</span>
Wyniki
</div>
</div>
<!-- Step 1: Input -->
<div id="aiStepInput" class="ai-step-content">
<div class="ai-input-section">
<div class="ai-input-tabs">
<button class="ai-input-tab active" onclick="switchAIInputTab('text', event)">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Tekst
</button>
<button class="ai-input-tab" onclick="switchAIInputTab('image', event)">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Screenshot
</button>
</div>
<!-- Text Input -->
<div id="aiTextInput">
<textarea id="aiUserText" class="ai-textarea" placeholder="Wklej dane użytkowników w dowolnym formacie, np.:
Jan Kowalski jan@pixlab.pl admin
Anna Nowak anna@test.pl
Piotr Wiśniewski piotr@waterm.pl user
Lub format CSV, Excel, lista emaili..."></textarea>
<p class="ai-hint">
<strong>Dowolne źródło i format</strong> — email, Excel, CRM, notatki, wiadomość ze Slacka...
AI przeanalizuje tekst i wyodrębni dane użytkowników.
</p>
</div>
<!-- Image Input -->
<div id="aiImageInput" style="display: none;">
<div class="ai-file-upload" id="aiFileDropzone" onclick="document.getElementById('aiFileInput').click()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p><strong>Wklej ze schowka:</strong>
<span style="display: inline-flex; align-items: center; gap: 3px; margin: 0 5px;">
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style="opacity: 0.5;"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"/></svg>
<code style="background: rgba(138,99,210,0.25); padding: 3px 8px; border-radius: 4px; font-size: 13px; font-weight: 600;">Ctrl+V</code>
</span>
<span style="display: inline-flex; align-items: center; gap: 3px; margin: 0 5px;">
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style="opacity: 0.5;"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
<code style="background: rgba(138,99,210,0.25); padding: 3px 8px; border-radius: 4px; font-size: 13px; font-weight: 600;">Cmd+V</code>
</span>
lub przeciągnij</p>
<p style="font-size: var(--font-size-sm);">PNG, JPG do 5MB • Możesz też kliknąć i wybrać plik</p>
</div>
<input type="file" id="aiFileInput" accept="image/*" style="display: none;" onchange="handleAIFileSelect(event)">
<div id="aiFilePreview" class="ai-file-preview" style="display: none;">
<img id="aiPreviewImg" src="" alt="Preview">
<div class="file-info">
<div class="file-name" id="aiFileName"></div>
<div class="file-size" id="aiFileSize"></div>
</div>
<button class="remove-file" onclick="removeAIFile()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<p class="ai-hint">AI odczyta tekst z obrazu i wyodrębni dane użytkowników.</p>
</div>
</div>
</div>
<!-- Step 2: Processing / Review -->
<div id="aiStepReview" class="ai-step-content" style="display: none;">
<!-- Loading state -->
<div id="aiLoading" class="ai-loading">
<div class="ai-spinner"></div>
<p class="ai-loading-text">AI analizuje dane...</p>
</div>
<!-- AI Response -->
<div id="aiResponse" style="display: none;">
<div class="ai-message assistant">
<div class="ai-message-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Gemini AI
</div>
<p id="aiAnalysisText"></p>
</div>
<!-- Duplicates warning -->
<div id="aiDuplicatesWarning" style="display: none; margin-bottom: var(--spacing-md);">
<div class="ai-message" style="background: #FEF3C7; border-left-color: #F59E0B;">
<div class="ai-message-header" style="color: #92400E;">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
Uwaga - duplikaty
</div>
<p id="aiDuplicatesText"></p>
</div>
</div>
<!-- Users table -->
<div id="aiUsersTableContainer">
<table class="ai-users-table">
<thead>
<tr>
<th style="width: 40px;"></th>
<th>Email</th>
<th>Nazwa</th>
<th>Firma</th>
<th>Admin</th>
<th></th>
</tr>
</thead>
<tbody id="aiUsersTableBody">
</tbody>
</table>
</div>
</div>
</div>
<!-- Step 3: Results -->
<div id="aiStepResults" class="ai-step-content" style="display: none;">
<div class="ai-message assistant">
<div class="ai-message-header">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Zakończono
</div>
<p id="aiResultsSummary"></p>
</div>
<div id="aiResultsList" class="ai-results-list">
</div>
</div>
<!-- Footer -->
<div class="ai-modal-footer">
<button id="aiBackBtn" class="btn btn-secondary" onclick="aiGoBack()" style="display: none;">
Wstecz
</button>
<div></div>
<button id="aiNextBtn" class="btn btn-primary" onclick="aiGoNext()">
Analizuj z AI
</button>
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
{% 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: '<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
error: '<path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
warning: '<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>'
};
toast.innerHTML = `
<svg class="toast-icon ${type}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
${iconSvg[type] || iconSvg.success}
</svg>
<span class="toast-message">${message}</span>
<button class="toast-close" onclick="this.parentElement.remove()">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
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: '<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>',
danger: '<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>',
success: '<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>'
};
icon.innerHTML = `<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">${iconSvg[iconType] || iconSvg.warning}</svg>`;
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.role === 'ADMIN';
const isVerified = row.dataset.isVerified === 'true';
let show = false;
const isPendingCompany = row.dataset.pendingCompany === 'true';
if (filter === 'all') show = true;
else if (filter === 'admin') show = isAdmin;
else if (filter === 'verified') show = isVerified;
else if (filter === 'unverified') show = !isVerified;
else if (filter === 'pending-company') show = isPendingCompany;
row.style.display = show ? '' : 'none';
});
});
});
async function changeUserRole(userId, newRole) {
const select = document.querySelector(`select.role-select[data-user-id="${userId}"]`);
const originalValue = select.dataset.originalValue || select.value;
try {
const response = await fetch('/admin/users-api/change-role', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
user_id: userId,
role: newRole
})
});
const data = await response.json();
if (data.success) {
select.dataset.originalValue = newRole;
showMessage(`Rola zmieniona na: ${getRoleLabel(newRole)}`, 'success');
// Update admin toggle button state if role changed to/from ADMIN
const row = select.closest('tr');
const adminBtn = row.querySelector('.admin-toggle');
if (adminBtn) {
if (newRole === 'ADMIN') {
adminBtn.classList.add('active');
} else {
adminBtn.classList.remove('active');
}
}
} else {
select.value = originalValue;
showMessage(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
select.value = originalValue;
showMessage('Błąd połączenia', 'error');
}
}
async function changeCompanyRole(userId, newRole) {
const selects = document.querySelectorAll(`select[data-user-id="${userId}"]`);
const select = Array.from(selects).find(s => s.onchange?.toString().includes('changeCompanyRole'));
if (!select) return;
const originalValue = select.dataset.originalValue || select.value;
try {
const response = await fetch('/admin/users-api/change-company-role', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
user_id: userId,
company_role: newRole
})
});
const data = await response.json();
if (data.success) {
select.dataset.originalValue = newRole;
const labels = {'NONE': 'Brak', 'VIEWER': 'Podgląd', 'EMPLOYEE': 'Pracownik', 'MANAGER': 'Zarządzający'};
showMessage(`Uprawnienia firmowe zmienione na: ${labels[newRole] || newRole}`, 'success');
} else {
select.value = originalValue;
showMessage(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
select.value = originalValue;
showMessage('Błąd połączenia', 'error');
}
}
function sendRoleNotification(userId, userName, userEmail) {
showConfirmModal(
'Wyślij powiadomienie',
`Wysłać email z podsumowaniem uprawnień do ${userName} (${userEmail})?`,
async () => {
try {
const response = await fetch(`/admin/users-api/${userId}/send-role-notification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
} else {
showToast(data.error || 'Błąd wysyłania', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
},
'success',
'Wyślij',
'btn-primary'
);
}
function getRoleLabel(role) {
const labels = {
'UNAFFILIATED': 'Niezrzeszony',
'MEMBER': 'Członek',
'EMPLOYEE': 'Pracownik',
'MANAGER': 'Kadra Zarządzająca',
'OFFICE_MANAGER': 'Kierownik Biura',
'ADMIN': 'Administrator'
};
return labels[role] || role;
}
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');
}
}
async function toggleRadaMember(userId) {
try {
const response = await fetch(`/admin/users/${userId}/toggle-rada-member`, {
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('addUserRole').value = 'MEMBER';
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 role = document.getElementById('addUserRole').value;
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,
role: role,
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');
}
}
let pwModalUserId = null;
function openPasswordModal(userId, userEmail) {
pwModalUserId = userId;
document.getElementById('pwModalEmail').textContent = userEmail;
document.getElementById('pwNewPassword').value = '';
document.getElementById('pwNewPassword').type = 'password';
document.getElementById('pwLinkResult').style.display = 'none';
document.getElementById('pwLinkActions').style.display = 'flex';
switchPwTab('set');
document.getElementById('passwordModal').classList.add('active');
}
function closePasswordModal() {
pwModalUserId = null;
document.getElementById('passwordModal').classList.remove('active');
}
function switchPwTab(tab) {
const setTab = document.getElementById('pwTabSet');
const linkTab = document.getElementById('pwTabLink');
const setPanel = document.getElementById('pwSetPanel');
const linkPanel = document.getElementById('pwLinkPanel');
if (tab === 'set') {
setTab.className = 'btn btn-primary';
linkTab.className = 'btn btn-secondary';
setPanel.style.display = '';
linkPanel.style.display = 'none';
} else {
setTab.className = 'btn btn-secondary';
linkTab.className = 'btn btn-primary';
setPanel.style.display = 'none';
linkPanel.style.display = '';
}
}
function generatePassword() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%^&*';
const array = new Uint32Array(16);
crypto.getRandomValues(array);
let pw = '';
for (let i = 0; i < 16; i++) {
pw += chars[array[i] % chars.length];
}
const input = document.getElementById('pwNewPassword');
input.value = pw;
input.type = 'text';
}
function togglePwVisibility() {
const input = document.getElementById('pwNewPassword');
input.type = input.type === 'password' ? 'text' : 'password';
}
async function saveDirectPassword() {
if (!pwModalUserId) return;
const password = document.getElementById('pwNewPassword').value.trim();
if (password.length < 8) {
showToast('Hasło musi mieć min. 8 znaków', 'error');
return;
}
try {
const response = await fetch(`/admin/users/${pwModalUserId}/set-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ password })
});
const data = await response.json();
if (data.success) {
showToast('Hasło zostało zmienione', 'success');
closePasswordModal();
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function generateResetLink() {
if (!pwModalUserId) return;
try {
const response = await fetch(`/admin/users/${pwModalUserId}/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('pwLinkResult').style.display = '';
document.getElementById('pwLinkActions').style.display = 'none';
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
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 = '<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">Nie znaleziono użytkowników</td></tr>';
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 = `
<td>
<input type="checkbox" class="ai-user-checkbox"
data-index="${index}"
${isDuplicate ? 'disabled' : 'checked'}>
</td>
<td>
${user.email}
${isDuplicate ? '<span class="ai-duplicate-badge">Duplikat</span>' : ''}
</td>
<td>${user.name || '-'}</td>
<td>${user.company_name || '-'}</td>
<td>${user.role === 'ADMIN' ? 'Tak' : 'Nie'}</td>
<td>
${hasWarnings ? `
<div class="ai-user-warning">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
${user.warnings.join(', ')}
</div>
` : ''}
</td>
`;
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 = `
<svg class="ai-result-icon success" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ai-result-info">
<span class="ai-result-email">${user.email}</span>
<span class="ai-result-password">${user.generated_password}</span>
</div>
<button class="ai-result-copy" onclick="copyPassword('${user.generated_password}', this)">
Kopiuj hasło
</button>
`;
container.appendChild(div);
});
// Failed users
results.failed.forEach(user => {
const div = document.createElement('div');
div.className = 'ai-result-item error';
div.innerHTML = `
<svg class="ai-result-icon error" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ai-result-info">
<span class="ai-result-email">${user.email}</span>
<span style="color: var(--error); font-size: var(--font-size-sm);">${user.error}</span>
</div>
`;
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 %}