nordabiz/templates/contacts/detail.html
Maciej Pienczyn 66856a697d refactor(phase1): Extract blueprints for reports, contacts, classifieds, calendar
Phase 1 of app.py refactoring - reducing from ~14,455 to ~13,699 lines.

New structure:
- blueprints/reports/ - 4 routes (/raporty/*)
- blueprints/community/contacts/ - 6 routes (/kontakty/*)
- blueprints/community/classifieds/ - 4 routes (/tablica/*)
- blueprints/community/calendar/ - 3 routes (/kalendarz/*)
- utils/ - decorators, helpers, notifications, analytics
- extensions.py - Flask extensions (csrf, login_manager, limiter)
- config.py - environment configurations

Updated templates with blueprint-prefixed url_for() calls.

⚠️ DO NOT DEPLOY before presentation on 2026-01-30 19:00

Tested on DEV: all endpoints working correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 10:10:45 +01:00

733 lines
24 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ contact.full_name }} - {{ contact.organization_name }} - Kontakty - Norda Biznes Hub{% endblock %}
{% block meta_description %}{{ contact.full_name }} - {{ contact.position or '' }} w {{ contact.organization_name }}. Kontakt zewnetrzny Norda Biznes.{% endblock %}
{% block extra_css %}
<style>
.back-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
text-decoration: none;
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-lg);
}
.back-link:hover {
color: var(--primary);
}
.contact-layout {
display: grid;
grid-template-columns: 1fr 350px;
gap: var(--spacing-2xl);
}
@media (max-width: 992px) {
.contact-layout {
grid-template-columns: 1fr;
}
}
.contact-main {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
.contact-header {
background: linear-gradient(135deg, var(--primary) 0%, #6b46c1 100%);
padding: var(--spacing-2xl);
color: white;
text-align: center;
}
.contact-avatar-large {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 700;
margin: 0 auto var(--spacing-lg);
border: 4px solid rgba(255,255,255,0.3);
overflow: hidden;
}
.contact-avatar-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.contact-name-large {
font-size: var(--font-size-2xl);
font-weight: 700;
margin-bottom: var(--spacing-xs);
}
.contact-position-large {
font-size: var(--font-size-lg);
opacity: 0.9;
margin-bottom: var(--spacing-sm);
}
.contact-org-large {
font-size: var(--font-size-base);
opacity: 0.8;
}
.org-type-badge-large {
display: inline-block;
padding: 4px 12px;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
background: rgba(255,255,255,0.2);
margin-top: var(--spacing-sm);
}
.contact-verified {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 4px 12px;
border-radius: var(--radius);
background: rgba(16, 185, 129, 0.3);
color: white;
font-size: var(--font-size-sm);
margin-top: var(--spacing-sm);
}
.contact-body {
padding: var(--spacing-xl);
}
.contact-section {
margin-bottom: var(--spacing-xl);
}
.contact-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 2px solid var(--primary);
}
.contact-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
}
@media (max-width: 576px) {
.contact-grid {
grid-template-columns: 1fr;
}
}
.contact-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
}
.contact-item-icon {
font-size: var(--font-size-xl);
width: 32px;
text-align: center;
flex-shrink: 0;
}
.contact-item-content {
flex: 1;
min-width: 0;
}
.contact-item-label {
font-size: var(--font-size-xs);
color: var(--text-muted);
margin-bottom: 2px;
}
.contact-item-value {
font-size: var(--font-size-base);
color: var(--text-primary);
word-break: break-word;
}
.contact-item-value a {
color: var(--primary);
text-decoration: none;
}
.contact-item-value a:hover {
text-decoration: underline;
}
.social-media-grid {
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.social-media-link {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
text-decoration: none;
color: white;
font-weight: 500;
transition: transform 0.2s ease;
}
.social-media-link:hover {
transform: translateY(-2px);
}
.social-media-link.linkedin { background: #0a66c2; }
.social-media-link.facebook { background: #1877f2; }
.social-media-link.twitter { background: #1da1f2; }
.related-links-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.related-link-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
text-decoration: none;
color: var(--text-primary);
transition: background 0.2s ease;
}
.related-link-item:hover {
background: var(--primary-bg);
}
.related-link-icon {
font-size: var(--font-size-lg);
}
.related-link-content {
flex: 1;
min-width: 0;
}
.related-link-title {
font-weight: 500;
color: var(--primary);
}
.related-link-type {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.tag {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--primary-bg);
color: var(--primary);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
.notes-content {
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
white-space: pre-wrap;
color: var(--text-secondary);
line-height: 1.6;
}
.project-box {
background: linear-gradient(135deg, var(--primary-bg) 0%, var(--surface) 100%);
border: 1px solid var(--primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
}
.project-name {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--primary);
margin-bottom: var(--spacing-sm);
}
.project-description {
color: var(--text-secondary);
line-height: 1.6;
}
/* Sidebar */
.sidebar {
position: sticky;
top: var(--spacing-xl);
}
.sidebar-section {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.sidebar-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 2px solid var(--primary);
}
.actions-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
text-decoration: none;
font-weight: 500;
border: none;
cursor: pointer;
width: 100%;
font-size: var(--font-size-base);
}
.action-btn.primary {
background: var(--primary);
color: white;
}
.action-btn.secondary {
background: var(--surface-secondary);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn.danger {
background: var(--danger-bg);
color: var(--danger);
border: 1px solid var(--danger);
}
.action-btn:hover {
opacity: 0.9;
}
.related-contact {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border);
}
.related-contact:last-child {
border-bottom: none;
padding-bottom: 0;
}
.related-contact:first-child {
padding-top: 0;
}
.related-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-sm);
font-weight: 600;
}
.related-info {
flex: 1;
min-width: 0;
}
.related-name {
font-weight: 500;
font-size: var(--font-size-sm);
}
.related-name a {
color: var(--text-primary);
text-decoration: none;
}
.related-name a:hover {
color: var(--primary);
}
.related-position {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.source-info {
font-size: var(--font-size-sm);
color: var(--text-muted);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
}
.source-info a {
color: var(--primary);
}
.meta-info {
font-size: var(--font-size-xs);
color: var(--text-muted);
margin-top: var(--spacing-md);
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<a href="{{ url_for('contacts.contacts_list') }}" class="back-link">
&larr; Powrot do listy kontaktow
</a>
<div class="contact-layout">
<!-- Main content -->
<div class="contact-main">
<div class="contact-header">
<div class="contact-avatar-large">
{% if contact.photo_url %}
<img src="{{ contact.photo_url }}" alt="{{ contact.full_name }}"
onerror="this.style.display='none'; this.parentElement.innerHTML='{{ contact.first_name[0]|upper }}';">
{% else %}
{{ contact.first_name[0]|upper }}
{% endif %}
</div>
<div class="contact-name-large">{{ contact.full_name }}</div>
{% if contact.position %}
<div class="contact-position-large">{{ contact.position }}</div>
{% endif %}
<div class="contact-org-large">{{ contact.organization_name }}</div>
<span class="org-type-badge-large">
{{ org_type_labels.get(contact.organization_type, contact.organization_type) }}
</span>
{% if contact.is_verified %}
<div class="contact-verified">
&#10003; Zweryfikowany
</div>
{% endif %}
</div>
<div class="contact-body">
<!-- Contact Info -->
{% if contact.has_contact_info or contact.phone_secondary %}
<div class="contact-section">
<h2 class="section-title">&#128222; Dane kontaktowe</h2>
<div class="contact-grid">
{% if contact.phone %}
<div class="contact-item">
<span class="contact-item-icon">&#128222;</span>
<div class="contact-item-content">
<div class="contact-item-label">Telefon</div>
<div class="contact-item-value">
<a href="tel:{{ contact.phone }}">{{ contact.phone }}</a>
</div>
</div>
</div>
{% endif %}
{% if contact.phone_secondary %}
<div class="contact-item">
<span class="contact-item-icon">&#128241;</span>
<div class="contact-item-content">
<div class="contact-item-label">Telefon dodatkowy</div>
<div class="contact-item-value">
<a href="tel:{{ contact.phone_secondary }}">{{ contact.phone_secondary }}</a>
</div>
</div>
</div>
{% endif %}
{% if contact.email %}
<div class="contact-item">
<span class="contact-item-icon">&#9993;</span>
<div class="contact-item-content">
<div class="contact-item-label">Email</div>
<div class="contact-item-value">
<a href="mailto:{{ contact.email }}">{{ contact.email }}</a>
</div>
</div>
</div>
{% endif %}
{% if contact.website %}
<div class="contact-item">
<span class="contact-item-icon">&#127760;</span>
<div class="contact-item-content">
<div class="contact-item-label">Strona WWW</div>
<div class="contact-item-value">
<a href="{{ contact.website }}" target="_blank" rel="noopener">{{ contact.website }}</a>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Social Media -->
{% if contact.has_social_media %}
<div class="contact-section">
<h2 class="section-title">&#128101; Media spolecznosciowe</h2>
<div class="social-media-grid">
{% if contact.linkedin_url %}
<a href="{{ contact.linkedin_url }}" target="_blank" rel="noopener"
class="social-media-link linkedin">
<span>in</span> LinkedIn
</a>
{% endif %}
{% if contact.facebook_url %}
<a href="{{ contact.facebook_url }}" target="_blank" rel="noopener"
class="social-media-link facebook">
<span>f</span> Facebook
</a>
{% endif %}
{% if contact.twitter_url %}
<a href="{{ contact.twitter_url }}" target="_blank" rel="noopener"
class="social-media-link twitter">
<span>X</span> Twitter/X
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- Organization -->
<div class="contact-section">
<h2 class="section-title">&#127970; Organizacja</h2>
<div class="contact-grid">
<div class="contact-item">
<span class="contact-item-icon">&#127970;</span>
<div class="contact-item-content">
<div class="contact-item-label">Nazwa</div>
<div class="contact-item-value">{{ contact.organization_name }}</div>
</div>
</div>
{% if contact.organization_address %}
<div class="contact-item">
<span class="contact-item-icon">&#128205;</span>
<div class="contact-item-content">
<div class="contact-item-label">Adres</div>
<div class="contact-item-value">{{ contact.organization_address }}</div>
</div>
</div>
{% endif %}
{% if contact.organization_website %}
<div class="contact-item">
<span class="contact-item-icon">&#127760;</span>
<div class="contact-item-content">
<div class="contact-item-label">Strona organizacji</div>
<div class="contact-item-value">
<a href="{{ contact.organization_website }}" target="_blank" rel="noopener">
{{ contact.organization_website }}
</a>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Project -->
{% if contact.project_name %}
<div class="contact-section">
<h2 class="section-title">&#128640; Projekt</h2>
<div class="project-box">
<div class="project-name">{{ contact.project_name }}</div>
{% if contact.project_description %}
<div class="project-description">{{ contact.project_description }}</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Related Links -->
{% if contact.related_links and contact.related_links|length > 0 %}
<div class="contact-section">
<h2 class="section-title">&#128279; Powiazane materialy</h2>
<div class="related-links-list">
{% for link in contact.related_links %}
<a href="{{ link.url }}" target="_blank" rel="noopener" class="related-link-item">
<span class="related-link-icon">
{% if link.type == 'article' %}&#128196;
{% elif link.type == 'document' %}&#128195;
{% elif link.type == 'video' %}&#127910;
{% else %}&#128279;{% endif %}
</span>
<div class="related-link-content">
<div class="related-link-title">{{ link.title }}</div>
<div class="related-link-type">{{ link.type|default('link', true) }}</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Tags -->
{% if contact.tags_list %}
<div class="contact-section">
<h2 class="section-title">&#127991; Tagi</h2>
<div class="tags-list">
{% for tag in contact.tags_list %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Notes -->
{% if contact.notes %}
<div class="contact-section">
<h2 class="section-title">&#128221; Notatki</h2>
<div class="notes-content">{{ contact.notes }}</div>
</div>
{% endif %}
<!-- Source -->
{% if contact.source_url %}
<div class="contact-section">
<h2 class="section-title">&#128214; Zrodlo</h2>
<div class="source-info">
<a href="{{ contact.source_url }}" target="_blank" rel="noopener">
{{ contact.source_url }}
</a>
</div>
</div>
{% endif %}
<div class="meta-info">
Dodano: {{ contact.created_at.strftime('%d.%m.%Y %H:%M') if contact.created_at else 'nieznana' }}
{% if contact.creator %}
przez {{ contact.creator.name or contact.creator.email }}
{% endif %}
{% if contact.updated_at and contact.updated_at != contact.created_at %}
| Zaktualizowano: {{ contact.updated_at.strftime('%d.%m.%Y %H:%M') }}
{% endif %}
</div>
</div>
</div>
<!-- Sidebar -->
<aside class="sidebar">
<!-- Actions -->
{% if can_edit %}
<div class="sidebar-section">
<h3 class="sidebar-title">Akcje</h3>
<div class="actions-list">
<a href="{{ url_for('contacts.contact_edit', contact_id=contact.id) }}" class="action-btn primary">
&#9998; Edytuj kontakt
</a>
<form action="{{ url_for('contacts.contact_delete', contact_id=contact.id) }}" method="POST"
onsubmit="return confirm('Czy na pewno chcesz usunac ten kontakt?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="action-btn danger">
&#128465; Usun kontakt
</button>
</form>
</div>
</div>
{% endif %}
<!-- Quick Contact -->
<div class="sidebar-section">
<h3 class="sidebar-title">Szybki kontakt</h3>
<div class="actions-list">
{% if contact.phone %}
<a href="tel:{{ contact.phone }}" class="action-btn secondary">
&#128222; Zadzwon
</a>
{% endif %}
{% if contact.email %}
<a href="mailto:{{ contact.email }}" class="action-btn secondary">
&#9993; Wyslij email
</a>
{% endif %}
</div>
</div>
<!-- Related contacts from same organization -->
{% if related_contacts %}
<div class="sidebar-section">
<h3 class="sidebar-title">Inni z {{ contact.organization_name|truncate(20) }}</h3>
{% for rc in related_contacts %}
<div class="related-contact">
<div class="related-avatar" style="background: hsl({{ (rc.id * 137) % 360 }}, 65%, 50%);">
{{ rc.first_name[0]|upper }}
</div>
<div class="related-info">
<div class="related-name">
<a href="{{ url_for('contacts.contact_detail', contact_id=rc.id) }}">{{ rc.full_name }}</a>
</div>
{% if rc.position %}
<div class="related-position">{{ rc.position }}</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Back to list -->
<div class="sidebar-section" style="text-align: center;">
<a href="{{ url_for('contacts.contacts_list') }}" class="action-btn secondary" style="width: 100%;">
&larr; Lista kontaktow
</a>
</div>
</aside>
</div>
</div>
{% endblock %}