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
- Add @role_required to 2 missing routes (krs_api PDF download, zopk milestones) - Add role-based menu visibility in admin bar (hide Users, Security, Benefits, Model Comparison, Debug from OFFICE_MANAGER users) - Inject SystemRole into Jinja2 context processor for template role checks - Replace is_admin checkbox with role select dropdown in user creation form - Migrate routes.py and routes_users_api.py from is_admin to SystemRole-based role assignment via set_role() - Add deprecation notice to is_admin database column - Add 23 RBAC unit tests (hierarchy, has_role, set_role, permissions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2577 lines
88 KiB
HTML
2577 lines
88 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: flex;
|
|
gap: var(--spacing-xs);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.btn-icon:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.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) {
|
|
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>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
|
|
{% if users %}
|
|
<table class="users-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Użytkownik</th>
|
|
<th>Firma</th>
|
|
<th>Rola</th>
|
|
<th>Utworzono</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' }}">
|
|
<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>
|
|
<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>
|
|
{% 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 '' }}')"
|
|
title="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 }})"
|
|
title="{{ '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 }})"
|
|
title="{{ '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 }})"
|
|
title="{{ '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' }})"
|
|
title="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="resetPassword({{ user.id }}, '{{ user.email|e }}')"
|
|
title="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>
|
|
|
|
<!-- Delete -->
|
|
<button class="btn-icon danger"
|
|
onclick="deleteUser({{ user.id }}, '{{ user.email|e }}')"
|
|
title="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>
|
|
|
|
<!-- Reset Password Modal -->
|
|
<div id="resetModal" 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">Link wygenerowany</div>
|
|
<div class="modal-description">
|
|
Poniższy link pozwoli użytkownikowi ustawić nowe hasło
|
|
</div>
|
|
<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 class="copy-btn-row">
|
|
<button class="btn btn-primary" onclick="copyResetUrl()">
|
|
<span id="copyBtnText">Skopiuj link</span>
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="closeResetModal()">Zamknij</button>
|
|
</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()" title="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;
|
|
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 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');
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
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 = '<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 %}
|