nordabiz/templates/contacts/form.html
Maciej Pienczyn 21a78befad feat(contacts): Baza kontaktów zewnętrznych dla członków Norda
- 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>
2026-01-27 08:35:06 +01:00

578 lines
23 KiB
HTML

{% extends "base.html" %}
{% block title %}{% if contact %}Edytuj kontakt{% else %}Dodaj kontakt{% endif %} - Norda Biznes Hub{% endblock %}
{% block extra_css %}
<style>
.form-container {
max-width: 900px;
margin: 0 auto;
}
.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);
}
.form-header {
margin-bottom: var(--spacing-xl);
}
.form-header h1 {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.form-header p {
color: var(--text-secondary);
}
.form-card {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
overflow: hidden;
}
.form-section-header {
background: var(--primary-bg);
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border);
}
.form-section-header h2 {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.form-section-body {
padding: var(--spacing-lg);
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group label {
display: block;
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.form-group label .required {
color: var(--danger);
}
.form-group input,
.form-group select,
.form-group textarea {
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);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-bg);
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.form-group .hint {
font-size: var(--font-size-xs);
color: var(--text-muted);
margin-top: var(--spacing-xs);
}
.form-group .input-with-icon {
position: relative;
}
.form-group .input-with-icon input {
padding-left: 40px;
}
.form-group .input-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: var(--font-size-lg);
}
/* Related Links Editor */
.related-links-editor {
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--spacing-md);
}
.related-links-list {
margin-bottom: var(--spacing-md);
}
.related-link-row {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--surface);
border-radius: var(--radius);
align-items: flex-start;
}
.related-link-row input,
.related-link-row select {
flex: 1;
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
.related-link-row input.link-title {
flex: 2;
}
.related-link-row input.link-url {
flex: 3;
}
.related-link-row select {
flex: 1;
min-width: 100px;
}
.remove-link-btn {
background: var(--danger-bg);
color: var(--danger);
border: none;
border-radius: var(--radius-sm);
padding: var(--spacing-xs) var(--spacing-sm);
cursor: pointer;
font-size: var(--font-size-sm);
}
.remove-link-btn:hover {
background: var(--danger);
color: white;
}
.add-link-btn {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
background: var(--primary-bg);
color: var(--primary);
border: 1px dashed var(--primary);
border-radius: var(--radius);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 500;
transition: background 0.2s ease;
}
.add-link-btn:hover {
background: var(--primary);
color: white;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg);
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.form-actions .btn-group {
display: flex;
gap: var(--spacing-sm);
}
@media (max-width: 576px) {
.form-actions {
flex-direction: column;
gap: var(--spacing-md);
}
.form-actions .btn-group {
width: 100%;
flex-direction: column;
}
.form-actions .btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="form-container">
<a href="{{ url_for('contacts_list') }}" class="back-link">
&larr; Powrot do listy kontaktow
</a>
<div class="form-header">
<h1>{% if contact %}&#9998; Edytuj kontakt{% else %}+ Dodaj nowy kontakt{% endif %}</h1>
<p>Kontakty zewnetrzne - osoby z urzedow, instytucji, agencji i partnerow projektow.</p>
</div>
<form method="POST" id="contact-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Personal Data -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128100; Dane osobowe</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group">
<label for="first_name">Imie <span class="required">*</span></label>
<input type="text" id="first_name" name="first_name" required
value="{{ contact.first_name if contact else '' }}"
placeholder="np. Anna">
</div>
<div class="form-group">
<label for="last_name">Nazwisko <span class="required">*</span></label>
<input type="text" id="last_name" name="last_name" required
value="{{ contact.last_name if contact else '' }}"
placeholder="np. Kowalska">
</div>
<div class="form-group">
<label for="position">Stanowisko</label>
<input type="text" id="position" name="position"
value="{{ contact.position if contact else '' }}"
placeholder="np. Specjalista ds. inwestycji">
</div>
<div class="form-group">
<label for="photo_url">URL zdjecia</label>
<input type="url" id="photo_url" name="photo_url"
value="{{ contact.photo_url if contact else '' }}"
placeholder="https://...">
<div class="hint">Opcjonalnie - link do zdjecia profilowego</div>
</div>
</div>
</div>
</div>
<!-- Contact Info -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128222; Dane kontaktowe</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group">
<label for="phone">Telefon</label>
<div class="input-with-icon">
<span class="input-icon">&#128222;</span>
<input type="tel" id="phone" name="phone"
value="{{ contact.phone if contact else '' }}"
placeholder="np. 58 32 33 160">
</div>
</div>
<div class="form-group">
<label for="phone_secondary">Telefon dodatkowy</label>
<div class="input-with-icon">
<span class="input-icon">&#128241;</span>
<input type="tel" id="phone_secondary" name="phone_secondary"
value="{{ contact.phone_secondary if contact else '' }}"
placeholder="np. 600 123 456">
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<div class="input-with-icon">
<span class="input-icon">&#9993;</span>
<input type="email" id="email" name="email"
value="{{ contact.email if contact else '' }}"
placeholder="np. a.kowalska@arp.gda.pl">
</div>
</div>
<div class="form-group">
<label for="website">Strona WWW osobista</label>
<div class="input-with-icon">
<span class="input-icon">&#127760;</span>
<input type="url" id="website" name="website"
value="{{ contact.website if contact else '' }}"
placeholder="https://...">
</div>
</div>
</div>
</div>
</div>
<!-- Social Media -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128101; Media spolecznosciowe</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group">
<label for="linkedin_url">LinkedIn</label>
<input type="url" id="linkedin_url" name="linkedin_url"
value="{{ contact.linkedin_url if contact else '' }}"
placeholder="https://linkedin.com/in/...">
</div>
<div class="form-group">
<label for="facebook_url">Facebook</label>
<input type="url" id="facebook_url" name="facebook_url"
value="{{ contact.facebook_url if contact else '' }}"
placeholder="https://facebook.com/...">
</div>
<div class="form-group">
<label for="twitter_url">Twitter/X</label>
<input type="url" id="twitter_url" name="twitter_url"
value="{{ contact.twitter_url if contact else '' }}"
placeholder="https://twitter.com/...">
</div>
</div>
</div>
</div>
<!-- Organization -->
<div class="form-card">
<div class="form-section-header">
<h2>&#127970; Organizacja</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group">
<label for="organization_name">Nazwa organizacji <span class="required">*</span></label>
<input type="text" id="organization_name" name="organization_name" required
value="{{ contact.organization_name if contact else '' }}"
placeholder="np. Agencja Rozwoju Pomorza S.A.">
</div>
<div class="form-group">
<label for="organization_type">Typ organizacji</label>
<select id="organization_type" name="organization_type">
{% for type_key in org_types %}
<option value="{{ type_key }}"
{% if contact and contact.organization_type == type_key %}selected{% endif %}>
{{ org_type_labels.get(type_key, type_key) }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group full-width">
<label for="organization_address">Adres organizacji</label>
<input type="text" id="organization_address" name="organization_address"
value="{{ contact.organization_address if contact else '' }}"
placeholder="np. ul. Arkońska 6, 80-387 Gdańsk">
</div>
<div class="form-group">
<label for="organization_website">Strona organizacji</label>
<input type="url" id="organization_website" name="organization_website"
value="{{ contact.organization_website if contact else '' }}"
placeholder="https://...">
</div>
<div class="form-group">
<label for="organization_logo_url">Logo organizacji (URL)</label>
<input type="url" id="organization_logo_url" name="organization_logo_url"
value="{{ contact.organization_logo_url if contact else '' }}"
placeholder="https://...">
</div>
</div>
</div>
</div>
<!-- Project -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128640; Projekt / Kontekst</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group">
<label for="project_name">Nazwa projektu</label>
<input type="text" id="project_name" name="project_name"
value="{{ contact.project_name if contact else '' }}"
placeholder="np. Elektrownia Jądrowa Choczewo, Tytani Przedsiębiorczości">
</div>
<div class="form-group full-width">
<label for="project_description">Opis kontekstu</label>
<textarea id="project_description" name="project_description"
placeholder="W jakim kontekscie poznalismy te osobe? Przy jakiej okazji?">{{ contact.project_description if contact else '' }}</textarea>
</div>
</div>
</div>
</div>
<!-- Related Links -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128279; Powiazane materialy</h2>
</div>
<div class="form-section-body">
<div class="form-group full-width">
<label>Artykuly, dokumenty, filmy</label>
<div class="related-links-editor">
<div class="related-links-list" id="related-links-list">
{% if contact and contact.related_links %}
{% for link in contact.related_links %}
<div class="related-link-row">
<input type="text" class="link-title" placeholder="Tytul"
value="{{ link.title }}">
<input type="url" class="link-url" placeholder="URL (https://...)"
value="{{ link.url }}">
<select class="link-type">
<option value="article" {% if link.type == 'article' %}selected{% endif %}>Artykul</option>
<option value="document" {% if link.type == 'document' %}selected{% endif %}>Dokument</option>
<option value="video" {% if link.type == 'video' %}selected{% endif %}>Film</option>
<option value="other" {% if link.type == 'other' %}selected{% endif %}>Inne</option>
</select>
<button type="button" class="remove-link-btn" onclick="removeLink(this)">&#10005;</button>
</div>
{% endfor %}
{% endif %}
</div>
<button type="button" class="add-link-btn" onclick="addLink()">
+ Dodaj link
</button>
</div>
<div class="hint">Dodaj linki do artykulow, dokumentow lub filmow zwiazanych z ta osoba</div>
</div>
<input type="hidden" name="related_links" id="related-links-json"
value="{{ contact.related_links|tojson if contact and contact.related_links else '[]' }}">
</div>
</div>
<!-- Additional Info -->
<div class="form-card">
<div class="form-section-header">
<h2>&#128221; Dodatkowe informacje</h2>
</div>
<div class="form-section-body">
<div class="form-grid">
<div class="form-group full-width">
<label for="tags">Tagi</label>
<input type="text" id="tags" name="tags"
value="{{ contact.tags if contact else '' }}"
placeholder="np. energetyka, inwestycje, Choczewo (oddzielone przecinkami)">
<div class="hint">Tagi pomagaja w wyszukiwaniu - oddziel przecinkami</div>
</div>
<div class="form-group full-width">
<label for="source_url">URL zrodla</label>
<input type="url" id="source_url" name="source_url"
value="{{ contact.source_url if contact else '' }}"
placeholder="Skad pochodzi informacja o tej osobie?">
</div>
<div class="form-group full-width">
<label for="notes">Notatki</label>
<textarea id="notes" name="notes" rows="4"
placeholder="Dodatkowe informacje, uwagi...">{{ contact.notes if contact else '' }}</textarea>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<a href="{{ url_for('contacts_list') }}" class="btn btn-secondary">
Anuluj
</a>
<div class="btn-group">
<button type="submit" class="btn btn-primary">
{% if contact %}Zapisz zmiany{% else %}Dodaj kontakt{% endif %}
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
// Related links management
function addLink() {
const list = document.getElementById('related-links-list');
const row = document.createElement('div');
row.className = 'related-link-row';
row.innerHTML = `
<input type="text" class="link-title" placeholder="Tytul">
<input type="url" class="link-url" placeholder="URL (https://...)">
<select class="link-type">
<option value="article">Artykul</option>
<option value="document">Dokument</option>
<option value="video">Film</option>
<option value="other">Inne</option>
</select>
<button type="button" class="remove-link-btn" onclick="removeLink(this)">&#10005;</button>
`;
list.appendChild(row);
}
function removeLink(btn) {
btn.parentElement.remove();
}
// Serialize related links before form submit
document.getElementById('contact-form').addEventListener('submit', function(e) {
const links = [];
const rows = document.querySelectorAll('.related-link-row');
rows.forEach(function(row) {
const title = row.querySelector('.link-title').value.trim();
const url = row.querySelector('.link-url').value.trim();
const type = row.querySelector('.link-type').value;
if (title && url) {
links.push({ title: title, url: url, type: type });
}
});
document.getElementById('related-links-json').value = JSON.stringify(links);
});
{% endblock %}