- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner" - Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash - Zachowano historyczne odniesienia w timeline i dokumentacji Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1570 lines
46 KiB
HTML
1570 lines
46 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Kontakty zewnetrzne - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block meta_description %}Baza kontaktow zewnetrznych - urzedy, instytucje, partnerzy projektow. Dostepna dla czlonkow Norda Biznes.{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.contacts-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.contacts-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-ai {
|
|
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.btn-ai:hover {
|
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
}
|
|
|
|
.contacts-filters {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.filters-row {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.filter-group {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.filter-group label {
|
|
display: block;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.filter-group input,
|
|
.filter-group select {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
background: var(--background);
|
|
}
|
|
|
|
.filter-group input:focus,
|
|
.filter-group select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px var(--primary-bg);
|
|
}
|
|
|
|
/* View toggle */
|
|
.view-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-md);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.stats-bar {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.view-toggle {
|
|
display: flex;
|
|
background: var(--surface);
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.view-toggle button {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.view-toggle button:hover {
|
|
background: var(--surface-secondary);
|
|
}
|
|
|
|
.view-toggle button.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.view-toggle button + button {
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
/* Card view */
|
|
.contacts-grid {
|
|
display: none;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.contacts-grid.active {
|
|
display: grid;
|
|
}
|
|
|
|
.contact-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.contact-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.contact-card-header {
|
|
padding: var(--spacing-lg);
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.contact-avatar {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.contact-avatar.has-photo {
|
|
background: none;
|
|
}
|
|
|
|
.contact-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.contact-avatar.small {
|
|
width: 40px;
|
|
height: 40px;
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.contact-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.contact-name {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.contact-name a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.contact-name a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.contact-position {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.contact-organization {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.org-type-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
margin-left: var(--spacing-xs);
|
|
}
|
|
|
|
.org-type-government { background: #dbeafe; color: #1e40af; }
|
|
.org-type-agency { background: #fce7f3; color: #9d174d; }
|
|
.org-type-company { background: #dcfce7; color: #166534; }
|
|
.org-type-ngo { background: #fef3c7; color: #92400e; }
|
|
.org-type-university { background: #f3e8ff; color: #6b21a8; }
|
|
.org-type-other { background: var(--surface-secondary); color: var(--text-secondary); }
|
|
|
|
.contact-card-body {
|
|
padding: 0 var(--spacing-lg) var(--spacing-lg);
|
|
}
|
|
|
|
.contact-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.contact-detail-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.contact-detail-item a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.contact-detail-item a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.contact-project {
|
|
margin-top: var(--spacing-sm);
|
|
padding-top: var(--spacing-sm);
|
|
border-top: 1px solid var(--border);
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.contact-project strong {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.social-links {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.social-link {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-decoration: none;
|
|
font-size: var(--font-size-sm);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.social-link:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.social-link.linkedin { background: #0a66c2; color: white; }
|
|
.social-link.facebook { background: #1877f2; color: white; }
|
|
.social-link.twitter { background: #1da1f2; color: white; }
|
|
|
|
/* Table view */
|
|
.contacts-table-wrapper {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
display: none;
|
|
}
|
|
|
|
.contacts-table-wrapper.active {
|
|
display: block;
|
|
}
|
|
|
|
.contacts-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.contacts-table th,
|
|
.contacts-table td {
|
|
padding: var(--spacing-md);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.contacts-table th {
|
|
background: var(--surface-secondary);
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.contacts-table tr:hover {
|
|
background: var(--surface-secondary);
|
|
}
|
|
|
|
.contacts-table .contact-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.contacts-table .contact-cell a {
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.contacts-table .contact-cell a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Organization group view */
|
|
.contacts-groups {
|
|
display: none;
|
|
}
|
|
|
|
.contacts-groups.active {
|
|
display: block;
|
|
}
|
|
|
|
.org-group {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: var(--spacing-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.org-group-header {
|
|
padding: var(--spacing-lg);
|
|
background: var(--surface-secondary);
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.org-group-header:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
.org-group-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.org-logo {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: var(--radius);
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.org-logo img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.org-details h3 {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.org-details .org-meta {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.org-group-toggle {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--surface);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.org-group.expanded .org-group-toggle {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.org-group-contacts {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease;
|
|
}
|
|
|
|
.org-group.expanded .org-group-contacts {
|
|
max-height: 2000px;
|
|
}
|
|
|
|
.org-contact-item {
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.org-contact-item:hover {
|
|
background: var(--surface-secondary);
|
|
}
|
|
|
|
.org-contact-main {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
flex: 1;
|
|
}
|
|
|
|
.org-contact-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.org-contact-details .name {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.org-contact-details .name a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.org-contact-details .name a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.org-contact-details .position {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.org-contact-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.org-contact-actions a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.org-contact-actions a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-3xl);
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: var(--font-size-xl);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.empty-state p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
/* Pagination */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.pagination a,
|
|
.pagination span {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pagination a {
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.pagination a:hover {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.pagination .current {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
/* Modal styles */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
max-width: 700px;
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-header h2 {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: var(--spacing-lg);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
/* AI form styles */
|
|
.ai-input-section {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.ai-input-section label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.ai-input-section .help-text {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.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);
|
|
box-shadow: 0 0 0 3px var(--primary-bg);
|
|
}
|
|
|
|
.ai-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin: var(--spacing-lg) 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.ai-divider::before,
|
|
.ai-divider::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.file-upload-area {
|
|
border: 2px dashed var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.file-upload-area:hover {
|
|
border-color: var(--primary);
|
|
background: var(--primary-bg);
|
|
}
|
|
|
|
.file-upload-area.dragover {
|
|
border-color: var(--primary);
|
|
background: var(--primary-bg);
|
|
}
|
|
|
|
.file-upload-area input[type="file"] {
|
|
display: none;
|
|
}
|
|
|
|
.file-upload-icon {
|
|
font-size: 2.5rem;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.file-upload-text {
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.file-upload-text strong {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.file-upload-hint {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.image-preview {
|
|
margin-top: var(--spacing-md);
|
|
display: none;
|
|
}
|
|
|
|
.image-preview.active {
|
|
display: block;
|
|
}
|
|
|
|
.image-preview img {
|
|
max-width: 100%;
|
|
max-height: 200px;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.image-preview .remove-image {
|
|
display: inline-block;
|
|
margin-top: var(--spacing-sm);
|
|
color: var(--danger);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* AI Results */
|
|
.ai-results {
|
|
display: none;
|
|
}
|
|
|
|
.ai-results.active {
|
|
display: block;
|
|
}
|
|
|
|
.ai-analysis {
|
|
background: var(--surface-secondary);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-lg);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.ai-contact-proposal {
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
margin-bottom: var(--spacing-md);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ai-contact-proposal-header {
|
|
padding: var(--spacing-md);
|
|
background: var(--surface-secondary);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.ai-contact-proposal-header label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.ai-contact-proposal-header label input {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.ai-contact-proposal-body {
|
|
padding: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.ai-contact-proposal-body .field {
|
|
display: flex;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.ai-contact-proposal-body .field-label {
|
|
color: var(--text-secondary);
|
|
min-width: 120px;
|
|
}
|
|
|
|
.ai-contact-proposal-body .field-value {
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Loading state */
|
|
.loading-overlay {
|
|
display: none;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
z-index: 10;
|
|
}
|
|
|
|
.loading-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.contacts-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.filter-group {
|
|
min-width: 100%;
|
|
}
|
|
|
|
.view-controls {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.contacts-table-wrapper {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.contacts-table {
|
|
min-width: 600px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<div class="contacts-header">
|
|
<h1>👥 Kontakty zewnetrzne</h1>
|
|
<div class="header-actions">
|
|
<button type="button" class="btn btn-ai" onclick="openAiModal()">
|
|
✨ Dodaj z AI
|
|
</button>
|
|
<a href="{{ url_for('contacts.contact_add') }}" class="btn btn-primary">
|
|
+ Dodaj kontakt
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contacts-filters">
|
|
<form method="GET" action="{{ url_for('contacts.contacts_list') }}">
|
|
<div class="filters-row">
|
|
<div class="filter-group" style="flex: 2;">
|
|
<label for="search">Szukaj</label>
|
|
<input type="text" id="search" name="q" value="{{ search }}"
|
|
placeholder="Imie, nazwisko, organizacja, projekt...">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="type">Typ organizacji</label>
|
|
<select id="type" name="type">
|
|
<option value="">Wszystkie</option>
|
|
{% for type_key in org_types %}
|
|
<option value="{{ type_key }}" {% if org_type == type_key %}selected{% endif %}>
|
|
{{ org_type_labels.get(type_key, type_key) }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="project">Projekt</label>
|
|
<select id="project" name="project">
|
|
<option value="">Wszystkie</option>
|
|
{% for proj in project_names %}
|
|
<option value="{{ proj }}" {% if project == proj %}selected{% endif %}>
|
|
{{ proj }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group" style="flex: 0;">
|
|
<button type="submit" class="btn btn-primary">Filtruj</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="view-controls">
|
|
<div class="stats-bar">
|
|
<span>Znaleziono: {{ total }} kontaktow</span>
|
|
{% if search or org_type or project %}
|
|
| <a href="{{ url_for('contacts.contacts_list') }}" style="color: var(--primary);">Wyczysc filtry</a>
|
|
{% endif %}
|
|
</div>
|
|
<div class="view-toggle">
|
|
<button type="button" data-view="groups" class="active" title="Pogrupowane po organizacji">
|
|
🏢 Organizacje
|
|
</button>
|
|
<button type="button" data-view="cards" title="Widok kart">
|
|
📄 Karty
|
|
</button>
|
|
<button type="button" data-view="table" title="Widok tabeli">
|
|
📋 Tabela
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{% if contacts %}
|
|
|
|
<!-- View: Organization Groups (default) -->
|
|
<div class="contacts-groups active" id="view-groups">
|
|
{% set contacts_by_org = {} %}
|
|
{% for contact in contacts %}
|
|
{% set org = contact.organization_name %}
|
|
{% if org not in contacts_by_org %}
|
|
{% set _ = contacts_by_org.update({org: {'contacts': [], 'type': contact.organization_type, 'logo': contact.organization_logo_url, 'website': contact.organization_website}}) %}
|
|
{% endif %}
|
|
{% set _ = contacts_by_org[org]['contacts'].append(contact) %}
|
|
{% endfor %}
|
|
|
|
{% for org_name, org_data in contacts_by_org.items() %}
|
|
<div class="org-group expanded">
|
|
<div class="org-group-header" onclick="toggleOrgGroup(this)">
|
|
<div class="org-group-info">
|
|
<div class="org-logo"
|
|
style="{% if not org_data.logo %}background: hsl({{ (org_name|length * 37) % 360 }}, 65%, 50%);{% endif %}">
|
|
{% if org_data.logo %}
|
|
<img src="{{ org_data.logo }}" alt="{{ org_name }}" onerror="this.style.display='none'; this.parentElement.textContent='{{ org_name[0]|upper }}';">
|
|
{% else %}
|
|
{{ org_name[0]|upper }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="org-details">
|
|
<h3>{{ org_name }}</h3>
|
|
<div class="org-meta">
|
|
<span class="org-type-badge org-type-{{ org_data.type }}">
|
|
{{ org_type_labels.get(org_data.type, org_data.type) }}
|
|
</span>
|
|
<span>{{ org_data.contacts|length }} {% if org_data.contacts|length == 1 %}kontakt{% elif org_data.contacts|length < 5 %}kontakty{% else %}kontaktow{% endif %}</span>
|
|
{% if org_data.website %}
|
|
<a href="{{ org_data.website }}" target="_blank" rel="noopener" onclick="event.stopPropagation();" style="color: var(--primary);">
|
|
🌐 Strona WWW
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="org-group-toggle">▼</div>
|
|
</div>
|
|
<div class="org-group-contacts">
|
|
{% for contact in org_data.contacts %}
|
|
<div class="org-contact-item">
|
|
<div class="org-contact-main">
|
|
<div class="contact-avatar small {% if contact.photo_url %}has-photo{% endif %}"
|
|
style="{% if not contact.photo_url %}background: hsl({{ (contact.id * 137) % 360 }}, 65%, 50%);{% endif %}">
|
|
{% if contact.photo_url %}
|
|
<img src="{{ contact.photo_url }}" alt="{{ contact.full_name }}"
|
|
onerror="this.parentElement.classList.remove('has-photo'); this.style.display='none'; this.parentElement.innerHTML='{{ contact.first_name[0]|upper }}';">
|
|
{% else %}
|
|
{{ contact.first_name[0]|upper }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="org-contact-details">
|
|
<div class="name">
|
|
<a href="{{ url_for('contacts.contact_detail', contact_id=contact.id) }}">
|
|
{{ contact.full_name }}
|
|
</a>
|
|
</div>
|
|
{% if contact.position %}
|
|
<div class="position">{{ contact.position }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="org-contact-actions">
|
|
{% if contact.phone %}
|
|
<a href="tel:{{ contact.phone }}">📞 {{ contact.phone }}</a>
|
|
{% endif %}
|
|
{% if contact.email %}
|
|
<a href="mailto:{{ contact.email }}">✉ Email</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('contacts.contact_detail', contact_id=contact.id) }}">Szczegoly →</a>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- View: Cards -->
|
|
<div class="contacts-grid" id="view-cards">
|
|
{% for contact in contacts %}
|
|
<div class="contact-card">
|
|
<div class="contact-card-header">
|
|
<div class="contact-avatar {% if contact.photo_url %}has-photo{% endif %}"
|
|
style="{% if not contact.photo_url %}background: hsl({{ (contact.id * 137) % 360 }}, 65%, 50%);{% endif %}">
|
|
{% if contact.photo_url %}
|
|
<img src="{{ contact.photo_url }}" alt="{{ contact.full_name }}"
|
|
onerror="this.parentElement.classList.remove('has-photo'); this.style.display='none'; this.parentElement.innerHTML='{{ contact.first_name[0]|upper }}';">
|
|
{% else %}
|
|
{{ contact.first_name[0]|upper }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="contact-info">
|
|
<div class="contact-name">
|
|
<a href="{{ url_for('contacts.contact_detail', contact_id=contact.id) }}">
|
|
{{ contact.full_name }}
|
|
</a>
|
|
</div>
|
|
{% if contact.position %}
|
|
<div class="contact-position">{{ contact.position }}</div>
|
|
{% endif %}
|
|
<div class="contact-organization">
|
|
{{ contact.organization_name }}
|
|
<span class="org-type-badge org-type-{{ contact.organization_type }}">
|
|
{{ org_type_labels.get(contact.organization_type, contact.organization_type) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="contact-card-body">
|
|
<div class="contact-details">
|
|
{% if contact.phone %}
|
|
<div class="contact-detail-item">
|
|
📞 <a href="tel:{{ contact.phone }}">{{ contact.phone }}</a>
|
|
</div>
|
|
{% endif %}
|
|
{% if contact.email %}
|
|
<div class="contact-detail-item">
|
|
✉ <a href="mailto:{{ contact.email }}">{{ contact.email }}</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if contact.has_social_media %}
|
|
<div class="social-links">
|
|
{% if contact.linkedin_url %}
|
|
<a href="{{ contact.linkedin_url }}" target="_blank" rel="noopener"
|
|
class="social-link linkedin" title="LinkedIn">in</a>
|
|
{% endif %}
|
|
{% if contact.facebook_url %}
|
|
<a href="{{ contact.facebook_url }}" target="_blank" rel="noopener"
|
|
class="social-link facebook" title="Facebook">f</a>
|
|
{% endif %}
|
|
{% if contact.twitter_url %}
|
|
<a href="{{ contact.twitter_url }}" target="_blank" rel="noopener"
|
|
class="social-link twitter" title="Twitter/X">X</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if contact.project_name %}
|
|
<div class="contact-project">
|
|
<strong>Projekt:</strong> {{ contact.project_name }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- View: Table -->
|
|
<div class="contacts-table-wrapper" id="view-table">
|
|
<table class="contacts-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Osoba</th>
|
|
<th>Organizacja</th>
|
|
<th>Stanowisko</th>
|
|
<th>Kontakt</th>
|
|
<th>Projekt</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for contact in contacts %}
|
|
<tr>
|
|
<td>
|
|
<div class="contact-cell">
|
|
<div class="contact-avatar small {% if contact.photo_url %}has-photo{% endif %}"
|
|
style="{% if not contact.photo_url %}background: hsl({{ (contact.id * 137) % 360 }}, 65%, 50%);{% endif %}">
|
|
{% if contact.photo_url %}
|
|
<img src="{{ contact.photo_url }}" alt="{{ contact.full_name }}"
|
|
onerror="this.parentElement.classList.remove('has-photo'); this.style.display='none'; this.parentElement.innerHTML='{{ contact.first_name[0]|upper }}';">
|
|
{% else %}
|
|
{{ contact.first_name[0]|upper }}
|
|
{% endif %}
|
|
</div>
|
|
<a href="{{ url_for('contacts.contact_detail', contact_id=contact.id) }}">
|
|
{{ contact.full_name }}
|
|
</a>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{{ contact.organization_name }}
|
|
<span class="org-type-badge org-type-{{ contact.organization_type }}">
|
|
{{ org_type_labels.get(contact.organization_type, contact.organization_type) }}
|
|
</span>
|
|
</td>
|
|
<td>{{ contact.position or '-' }}</td>
|
|
<td>
|
|
{% if contact.phone %}
|
|
<a href="tel:{{ contact.phone }}">{{ contact.phone }}</a>
|
|
{% endif %}
|
|
{% if contact.phone and contact.email %}<br>{% endif %}
|
|
{% if contact.email %}
|
|
<a href="mailto:{{ contact.email }}">{{ contact.email }}</a>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ contact.project_name or '-' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% if page > 1 %}
|
|
<a href="{{ url_for('contacts.contacts_list', page=page-1, q=search, type=org_type, project=project) }}">← Poprzednia</a>
|
|
{% endif %}
|
|
|
|
{% for p in range(1, total_pages + 1) %}
|
|
{% if p == page %}
|
|
<span class="current">{{ p }}</span>
|
|
{% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
|
|
<a href="{{ url_for('contacts.contacts_list', page=p, q=search, type=org_type, project=project) }}">{{ p }}</a>
|
|
{% elif p == page - 3 or p == page + 3 %}
|
|
<span>...</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page < total_pages %}
|
|
<a href="{{ url_for('contacts.contacts_list', page=page+1, q=search, type=org_type, project=project) }}">Nastepna →</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📋</div>
|
|
<h3>Brak kontaktow</h3>
|
|
<p>
|
|
{% if search or org_type or project %}
|
|
Nie znaleziono kontaktow pasujacych do podanych kryteriow.
|
|
{% else %}
|
|
Baza kontaktow zewnetrznych jest pusta. Dodaj pierwszy kontakt!
|
|
{% endif %}
|
|
</p>
|
|
<div class="header-actions" style="justify-content: center;">
|
|
<button type="button" class="btn btn-ai" onclick="openAiModal()">
|
|
✨ Dodaj z AI
|
|
</button>
|
|
<a href="{{ url_for('contacts.contact_add') }}" class="btn btn-primary">
|
|
+ Dodaj pierwszy kontakt
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Modal: Dodaj z AI -->
|
|
<div class="modal-overlay" id="aiModal">
|
|
<div class="modal" style="position: relative;">
|
|
<div class="loading-overlay" id="aiLoading">
|
|
<div class="loading-spinner"></div>
|
|
<div>Analizuje z AI...</div>
|
|
</div>
|
|
|
|
<div class="modal-header">
|
|
<h2>✨ Dodaj kontakty z AI</h2>
|
|
<button type="button" class="modal-close" onclick="closeAiModal()">×</button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<!-- Input section -->
|
|
<div id="aiInputSection">
|
|
<div class="ai-input-section">
|
|
<label for="aiText">Wklej tekst z danymi kontaktowymi</label>
|
|
<p class="help-text">
|
|
Wklej email, wizytowke, tekst ze strony internetowej lub dowolny tekst zawierajacy dane kontaktowe.
|
|
AI wyodrębni informacje o osobach.
|
|
</p>
|
|
<textarea id="aiText" class="ai-textarea"
|
|
placeholder="Przyklad:
|
|
|
|
Anna Kowalska
|
|
Dyrektor ds. Rozwoju
|
|
Agencja Rozwoju Pomorza S.A.
|
|
tel. 58 32 33 160
|
|
anna.kowalska@arp.gda.pl"></textarea>
|
|
</div>
|
|
|
|
<div class="ai-divider">lub</div>
|
|
|
|
<div class="ai-input-section">
|
|
<label>Dodaj zrzut ekranu / zdjecie wizytowki</label>
|
|
<div class="file-upload-area" id="fileUploadArea">
|
|
<input type="file" id="aiImage" accept="image/*">
|
|
<div class="file-upload-icon">📷</div>
|
|
<div class="file-upload-text">
|
|
<strong>Kliknij aby wybrac</strong> lub przeciagnij plik
|
|
</div>
|
|
<div class="file-upload-hint">PNG, JPG, WebP (max 10MB)</div>
|
|
</div>
|
|
<div class="image-preview" id="imagePreview">
|
|
<img id="previewImg" src="" alt="Podglad">
|
|
<div class="remove-image" onclick="removeImage()">✖ Usun obrazek</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results section -->
|
|
<div class="ai-results" id="aiResults">
|
|
<div class="ai-analysis" id="aiAnalysis"></div>
|
|
<div id="aiProposals"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeAiModal()">Anuluj</button>
|
|
<button type="button" class="btn btn-ai" id="aiParseBtn" onclick="parseWithAi()">
|
|
✨ Analizuj z AI
|
|
</button>
|
|
<button type="button" class="btn btn-primary" id="aiSaveBtn" onclick="saveSelectedContacts()" style="display: none;">
|
|
Zapisz wybrane kontakty
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// View toggle
|
|
const viewToggleBtns = document.querySelectorAll('.view-toggle button');
|
|
const viewContainers = {
|
|
'groups': document.getElementById('view-groups'),
|
|
'cards': document.getElementById('view-cards'),
|
|
'table': document.getElementById('view-table')
|
|
};
|
|
|
|
// Load saved view preference
|
|
const savedView = localStorage.getItem('contacts_view') || 'groups';
|
|
switchView(savedView);
|
|
|
|
viewToggleBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const view = btn.dataset.view;
|
|
switchView(view);
|
|
localStorage.setItem('contacts_view', view);
|
|
});
|
|
});
|
|
|
|
function switchView(view) {
|
|
// Update buttons
|
|
viewToggleBtns.forEach(b => b.classList.remove('active'));
|
|
document.querySelector(`[data-view="${view}"]`)?.classList.add('active');
|
|
|
|
// Update containers
|
|
Object.keys(viewContainers).forEach(v => {
|
|
if (viewContainers[v]) {
|
|
viewContainers[v].classList.remove('active');
|
|
if (v === view) {
|
|
viewContainers[v].classList.add('active');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Organization group toggle
|
|
function toggleOrgGroup(header) {
|
|
const group = header.closest('.org-group');
|
|
group.classList.toggle('expanded');
|
|
}
|
|
|
|
// AI Modal
|
|
const aiModal = document.getElementById('aiModal');
|
|
const aiLoading = document.getElementById('aiLoading');
|
|
const aiInputSection = document.getElementById('aiInputSection');
|
|
const aiResults = document.getElementById('aiResults');
|
|
const aiParseBtn = document.getElementById('aiParseBtn');
|
|
const aiSaveBtn = document.getElementById('aiSaveBtn');
|
|
let parsedContacts = [];
|
|
|
|
function openAiModal() {
|
|
aiModal.classList.add('active');
|
|
resetAiModal();
|
|
}
|
|
|
|
function closeAiModal() {
|
|
aiModal.classList.remove('active');
|
|
resetAiModal();
|
|
}
|
|
|
|
function resetAiModal() {
|
|
document.getElementById('aiText').value = '';
|
|
removeImage();
|
|
aiInputSection.style.display = 'block';
|
|
aiResults.classList.remove('active');
|
|
aiParseBtn.style.display = 'inline-flex';
|
|
aiSaveBtn.style.display = 'none';
|
|
parsedContacts = [];
|
|
}
|
|
|
|
// File upload handling
|
|
const fileUploadArea = document.getElementById('fileUploadArea');
|
|
const aiImageInput = document.getElementById('aiImage');
|
|
const imagePreview = document.getElementById('imagePreview');
|
|
const previewImg = document.getElementById('previewImg');
|
|
|
|
fileUploadArea.addEventListener('click', () => aiImageInput.click());
|
|
|
|
fileUploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
fileUploadArea.classList.add('dragover');
|
|
});
|
|
|
|
fileUploadArea.addEventListener('dragleave', () => {
|
|
fileUploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
fileUploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
fileUploadArea.classList.remove('dragover');
|
|
const file = e.dataTransfer.files[0];
|
|
if (file && file.type.startsWith('image/')) {
|
|
handleImageFile(file);
|
|
}
|
|
});
|
|
|
|
aiImageInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
handleImageFile(file);
|
|
}
|
|
});
|
|
|
|
function handleImageFile(file) {
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
alert('Plik jest za duzy. Maksymalny rozmiar to 10MB.');
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
previewImg.src = e.target.result;
|
|
imagePreview.classList.add('active');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function removeImage() {
|
|
aiImageInput.value = '';
|
|
previewImg.src = '';
|
|
imagePreview.classList.remove('active');
|
|
}
|
|
|
|
// AI parsing
|
|
async function parseWithAi() {
|
|
const text = document.getElementById('aiText').value.trim();
|
|
const imageData = previewImg.src && previewImg.src.startsWith('data:') ? previewImg.src : null;
|
|
|
|
if (!text && !imageData) {
|
|
alert('Wklej tekst lub dodaj obrazek do analizy.');
|
|
return;
|
|
}
|
|
|
|
aiLoading.classList.add('active');
|
|
|
|
try {
|
|
const response = await fetch('/api/contacts/ai-parse', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
text: text || null,
|
|
image_data: imageData
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Blad analizy AI');
|
|
}
|
|
|
|
displayAiResults(data);
|
|
|
|
} catch (error) {
|
|
alert('Blad: ' + error.message);
|
|
} finally {
|
|
aiLoading.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function displayAiResults(data) {
|
|
parsedContacts = data.contacts || [];
|
|
|
|
if (parsedContacts.length === 0) {
|
|
alert('AI nie znalazlo zadnych kontaktow w podanych danych.');
|
|
return;
|
|
}
|
|
|
|
// Show results section
|
|
aiInputSection.style.display = 'none';
|
|
aiResults.classList.add('active');
|
|
aiParseBtn.style.display = 'none';
|
|
aiSaveBtn.style.display = 'inline-flex';
|
|
|
|
// Display analysis
|
|
document.getElementById('aiAnalysis').innerHTML =
|
|
`<strong>Analiza AI:</strong> ${data.analysis || 'Znaleziono ' + parsedContacts.length + ' kontaktow.'}`;
|
|
|
|
// Display contact proposals
|
|
const proposalsHtml = parsedContacts.map((contact, index) => `
|
|
<div class="ai-contact-proposal">
|
|
<div class="ai-contact-proposal-header">
|
|
<label>
|
|
<input type="checkbox" checked data-index="${index}">
|
|
<strong>${contact.first_name} ${contact.last_name}</strong>
|
|
</label>
|
|
<span class="org-type-badge org-type-${contact.organization_type || 'other'}">
|
|
${getOrgTypeLabel(contact.organization_type)}
|
|
</span>
|
|
</div>
|
|
<div class="ai-contact-proposal-body">
|
|
<div class="field">
|
|
<span class="field-label">Organizacja:</span>
|
|
<span class="field-value">${contact.organization_name || '-'}</span>
|
|
</div>
|
|
${contact.position ? `<div class="field">
|
|
<span class="field-label">Stanowisko:</span>
|
|
<span class="field-value">${contact.position}</span>
|
|
</div>` : ''}
|
|
${contact.phone ? `<div class="field">
|
|
<span class="field-label">Telefon:</span>
|
|
<span class="field-value">${contact.phone}</span>
|
|
</div>` : ''}
|
|
${contact.email ? `<div class="field">
|
|
<span class="field-label">Email:</span>
|
|
<span class="field-value">${contact.email}</span>
|
|
</div>` : ''}
|
|
${contact.project_name ? `<div class="field">
|
|
<span class="field-label">Projekt:</span>
|
|
<span class="field-value">${contact.project_name}</span>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
document.getElementById('aiProposals').innerHTML = proposalsHtml;
|
|
}
|
|
|
|
function getOrgTypeLabel(type) {
|
|
const labels = {
|
|
'government': 'Urzad',
|
|
'agency': 'Agencja',
|
|
'company': 'Firma',
|
|
'ngo': 'NGO',
|
|
'university': 'Uczelnia',
|
|
'other': 'Inne'
|
|
};
|
|
return labels[type] || type || 'Inne';
|
|
}
|
|
|
|
async function saveSelectedContacts() {
|
|
const checkboxes = document.querySelectorAll('#aiProposals input[type="checkbox"]:checked');
|
|
const selectedIndices = Array.from(checkboxes).map(cb => parseInt(cb.dataset.index));
|
|
|
|
if (selectedIndices.length === 0) {
|
|
alert('Wybierz co najmniej jeden kontakt do zapisania.');
|
|
return;
|
|
}
|
|
|
|
const contactsToSave = selectedIndices.map(i => parsedContacts[i]);
|
|
|
|
aiLoading.classList.add('active');
|
|
|
|
try {
|
|
const response = await fetch('/api/contacts/bulk-create', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({ contacts: contactsToSave })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Blad zapisywania kontaktow');
|
|
}
|
|
|
|
alert(`Zapisano ${data.created} kontaktow!`);
|
|
closeAiModal();
|
|
window.location.reload();
|
|
|
|
} catch (error) {
|
|
alert('Blad: ' + error.message);
|
|
} finally {
|
|
aiLoading.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// Close modal on overlay click
|
|
aiModal.addEventListener('click', (e) => {
|
|
if (e.target === aiModal) {
|
|
closeAiModal();
|
|
}
|
|
});
|
|
|
|
// Close modal on Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && aiModal.classList.contains('active')) {
|
|
closeAiModal();
|
|
}
|
|
});
|
|
{% endblock %}
|