- Nowy model ExternalContact z pełnymi danymi kontaktowymi, social media, related_links (JSONB) - Migracja SQL 020_external_contacts.sql z full-text search - Routes: /kontakty (lista), /kontakty/dodaj, /kontakty/<id>, /kontakty/<id>/edytuj - Szablony: lista z filtrami, karta szczegółów, formularz CRUD - Nawigacja: link "Kontakty zewnętrzne" w dropdown Społeczność - Poprawka: aktualizacja liczby firm z 80 na 111 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
475 lines
14 KiB
HTML
475 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Kontakty zewnetrzne - Norda Biznes Hub{% 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);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.contacts-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.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-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; }
|
|
|
|
.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 {
|
|
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;
|
|
}
|
|
|
|
.stats-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.contacts-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.filter-group {
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<div class="contacts-header">
|
|
<h1>👥 Kontakty zewnetrzne</h1>
|
|
<a href="{{ url_for('contact_add') }}" class="btn btn-primary">
|
|
+ Dodaj kontakt
|
|
</a>
|
|
</div>
|
|
|
|
<div class="contacts-filters">
|
|
<form method="GET" action="{{ url_for('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="stats-bar">
|
|
<span>Znaleziono: {{ total }} kontaktow</span>
|
|
{% if search or org_type or project %}
|
|
<a href="{{ url_for('contacts_list') }}" style="color: var(--primary);">Wyczysc filtry</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if contacts %}
|
|
<div class="contacts-grid">
|
|
{% 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('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>
|
|
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% if page > 1 %}
|
|
<a href="{{ url_for('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_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_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>
|
|
<a href="{{ url_for('contact_add') }}" class="btn btn-primary">
|
|
+ Dodaj pierwszy kontakt
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|