- 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>
578 lines
23 KiB
HTML
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">
|
|
← Powrot do listy kontaktow
|
|
</a>
|
|
|
|
<div class="form-header">
|
|
<h1>{% if contact %}✎ 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>👤 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>📞 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">📞</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">📱</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">✉</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">🌐</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>👥 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>🏢 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>🚀 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>🔗 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)">✕</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>📝 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)">✕</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 %}
|