From 21a78befad79a269c7bb39f4b64d132798482f7f Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Tue, 27 Jan 2026 08:35:06 +0100 Subject: [PATCH] =?UTF-8?q?feat(contacts):=20Baza=20kontakt=C3=B3w=20zewn?= =?UTF-8?q?=C4=99trznych=20dla=20cz=C5=82onk=C3=B3w=20Norda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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/, /kontakty//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 --- CLAUDE.md | 2 +- app.py | 300 ++++++- database.py | 111 +++ database/migrations/020_external_contacts.sql | 96 +++ templates/base.html | 1 + templates/chat.html | 2 +- templates/contacts/detail.html | 732 ++++++++++++++++++ templates/contacts/form.html | 577 ++++++++++++++ templates/contacts/list.html | 474 ++++++++++++ 9 files changed, 2290 insertions(+), 5 deletions(-) create mode 100644 database/migrations/020_external_contacts.sql create mode 100644 templates/contacts/detail.html create mode 100644 templates/contacts/form.html create mode 100644 templates/contacts/list.html diff --git a/CLAUDE.md b/CLAUDE.md index 49c854f..725b077 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Platforma katalogowa i networkingowa dla członków stowarzyszenia Norda Biznes z Wejherowa. - **Produkcja:** https://nordabiznes.pl - **Status:** LIVE (od 2025-11-23) -- **Firmy:** 80 członków Norda Biznes (100% pokrycia) +- **Firmy:** 111 członków Norda Biznes ## Struktura projektu diff --git a/app.py b/app.py index 4baf115..2621dd6 100644 --- a/app.py +++ b/app.py @@ -4683,7 +4683,7 @@ def chat_send_message(conversation_id): # Technical metadata 'tech_info': { 'model': 'gemini-2.0-flash', - 'data_source': 'PostgreSQL (80 firm Norda Biznes)', + 'data_source': 'PostgreSQL (111 firm Norda Biznes)', 'architecture': 'Full DB Context (wszystkie firmy w kontekście AI)', 'tokens_input': tokens_in, 'tokens_output': tokens_out, @@ -9862,7 +9862,7 @@ def release_notes(): 'new': [ 'Audyt GBP: Sekcja edukacyjna "Jak działa wizytówka Google?" z trzema kartami', 'Audyt GBP: Przycisk "Zobacz wizytówkę Google" prowadzący do profilu w Maps', - 'Audyt GBP: Pełny audyt z Google Places API dla wszystkich 80 firm', + 'Audyt GBP: Pełny audyt z Google Places API dla wszystkich 111 firm', 'Audyty: Klikalne banery wyników prowadzą do szczegółowych stron audytu', 'Audyty: Sekcje audytów inline na profilu firmy (SEO, GBP, Social Media, IT)', ], @@ -10046,7 +10046,7 @@ def release_notes(): 'badges': ['new'], 'new': [ 'Oficjalny start platformy Norda Biznes Hub', - 'Katalog 80 firm członkowskich', + 'Katalog 111 firm członkowskich', 'Wyszukiwarka firm po nazwie, kategorii, usługach', 'Profile firm z pełnymi danymi kontaktowymi', ], @@ -13817,6 +13817,300 @@ def announcement_detail(slug): db.close() +# ============================================================ +# EXTERNAL CONTACTS (Kontakty zewnętrzne) +# ============================================================ + +@app.route('/kontakty') +@login_required +def contacts_list(): + """ + Lista kontaktów zewnętrznych - urzędy, instytucje, partnerzy. + Dostępna dla wszystkich zalogowanych członków. + """ + from database import ExternalContact, User + + db = SessionLocal() + try: + page = request.args.get('page', 1, type=int) + per_page = 20 + search = request.args.get('q', '').strip() + org_type = request.args.get('type', '') + project = request.args.get('project', '') + + query = db.query(ExternalContact).filter(ExternalContact.is_active == True) + + # Search filter + if search: + search_pattern = f'%{search}%' + query = query.filter( + or_( + ExternalContact.first_name.ilike(search_pattern), + ExternalContact.last_name.ilike(search_pattern), + ExternalContact.organization_name.ilike(search_pattern), + ExternalContact.position.ilike(search_pattern), + ExternalContact.project_name.ilike(search_pattern), + ExternalContact.tags.ilike(search_pattern) + ) + ) + + # Organization type filter + if org_type and org_type in ExternalContact.ORGANIZATION_TYPES: + query = query.filter(ExternalContact.organization_type == org_type) + + # Project filter + if project: + query = query.filter(ExternalContact.project_name.ilike(f'%{project}%')) + + # Order by organization name, then last name + query = query.order_by( + ExternalContact.organization_name, + ExternalContact.last_name + ) + + # Pagination + total = query.count() + contacts = query.offset((page - 1) * per_page).limit(per_page).all() + total_pages = (total + per_page - 1) // per_page + + # Get unique projects for filter dropdown + projects = db.query(ExternalContact.project_name).filter( + ExternalContact.is_active == True, + ExternalContact.project_name.isnot(None), + ExternalContact.project_name != '' + ).distinct().order_by(ExternalContact.project_name).all() + project_names = [p[0] for p in projects if p[0]] + + return render_template('contacts/list.html', + contacts=contacts, + page=page, + total_pages=total_pages, + total=total, + search=search, + org_type=org_type, + project=project, + org_types=ExternalContact.ORGANIZATION_TYPES, + org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS, + project_names=project_names) + + finally: + db.close() + + +@app.route('/kontakty/') +@login_required +def contact_detail(contact_id): + """ + Szczegóły kontaktu zewnętrznego - pełna karta osoby. + """ + from database import ExternalContact + + db = SessionLocal() + try: + contact = db.query(ExternalContact).filter( + ExternalContact.id == contact_id, + ExternalContact.is_active == True + ).first() + + if not contact: + flash('Kontakt nie został znaleziony.', 'error') + return redirect(url_for('contacts_list')) + + # Get other contacts from the same organization + related_contacts = db.query(ExternalContact).filter( + ExternalContact.organization_name == contact.organization_name, + ExternalContact.id != contact.id, + ExternalContact.is_active == True + ).order_by(ExternalContact.last_name).limit(5).all() + + # Check if current user can edit (creator or admin) + can_edit = (current_user.is_admin or + (contact.created_by and contact.created_by == current_user.id)) + + return render_template('contacts/detail.html', + contact=contact, + related_contacts=related_contacts, + can_edit=can_edit, + org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS) + + finally: + db.close() + + +@app.route('/kontakty/dodaj', methods=['GET', 'POST']) +@login_required +def contact_add(): + """ + Dodawanie nowego kontaktu zewnętrznego. + Każdy zalogowany użytkownik może dodać kontakt. + """ + from database import ExternalContact + + if request.method == 'POST': + db = SessionLocal() + try: + # Parse related_links from form (JSON) + related_links_json = request.form.get('related_links', '[]') + try: + related_links = json.loads(related_links_json) if related_links_json else [] + except json.JSONDecodeError: + related_links = [] + + contact = ExternalContact( + first_name=request.form.get('first_name', '').strip(), + last_name=request.form.get('last_name', '').strip(), + position=request.form.get('position', '').strip() or None, + photo_url=request.form.get('photo_url', '').strip() or None, + phone=request.form.get('phone', '').strip() or None, + phone_secondary=request.form.get('phone_secondary', '').strip() or None, + email=request.form.get('email', '').strip() or None, + website=request.form.get('website', '').strip() or None, + linkedin_url=request.form.get('linkedin_url', '').strip() or None, + facebook_url=request.form.get('facebook_url', '').strip() or None, + twitter_url=request.form.get('twitter_url', '').strip() or None, + organization_name=request.form.get('organization_name', '').strip(), + organization_type=request.form.get('organization_type', 'other'), + organization_address=request.form.get('organization_address', '').strip() or None, + organization_website=request.form.get('organization_website', '').strip() or None, + organization_logo_url=request.form.get('organization_logo_url', '').strip() or None, + project_name=request.form.get('project_name', '').strip() or None, + project_description=request.form.get('project_description', '').strip() or None, + source_type='manual', + source_url=request.form.get('source_url', '').strip() or None, + related_links=related_links, + tags=request.form.get('tags', '').strip() or None, + notes=request.form.get('notes', '').strip() or None, + created_by=current_user.id + ) + + db.add(contact) + db.commit() + + flash(f'Kontakt {contact.full_name} został dodany.', 'success') + return redirect(url_for('contact_detail', contact_id=contact.id)) + + except Exception as e: + db.rollback() + app.logger.error(f"Error adding contact: {e}") + flash('Wystąpił błąd podczas dodawania kontaktu.', 'error') + + finally: + db.close() + + # GET - show form + return render_template('contacts/form.html', + contact=None, + org_types=ExternalContact.ORGANIZATION_TYPES, + org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS) + + +@app.route('/kontakty//edytuj', methods=['GET', 'POST']) +@login_required +def contact_edit(contact_id): + """ + Edycja kontaktu zewnętrznego. + Może edytować twórca kontaktu lub admin. + """ + from database import ExternalContact + + db = SessionLocal() + try: + contact = db.query(ExternalContact).filter( + ExternalContact.id == contact_id + ).first() + + if not contact: + flash('Kontakt nie został znaleziony.', 'error') + return redirect(url_for('contacts_list')) + + # Check permissions + if not current_user.is_admin and contact.created_by != current_user.id: + flash('Nie masz uprawnień do edycji tego kontaktu.', 'error') + return redirect(url_for('contact_detail', contact_id=contact_id)) + + if request.method == 'POST': + # Parse related_links from form (JSON) + related_links_json = request.form.get('related_links', '[]') + try: + related_links = json.loads(related_links_json) if related_links_json else [] + except json.JSONDecodeError: + related_links = contact.related_links or [] + + contact.first_name = request.form.get('first_name', '').strip() + contact.last_name = request.form.get('last_name', '').strip() + contact.position = request.form.get('position', '').strip() or None + contact.photo_url = request.form.get('photo_url', '').strip() or None + contact.phone = request.form.get('phone', '').strip() or None + contact.phone_secondary = request.form.get('phone_secondary', '').strip() or None + contact.email = request.form.get('email', '').strip() or None + contact.website = request.form.get('website', '').strip() or None + contact.linkedin_url = request.form.get('linkedin_url', '').strip() or None + contact.facebook_url = request.form.get('facebook_url', '').strip() or None + contact.twitter_url = request.form.get('twitter_url', '').strip() or None + contact.organization_name = request.form.get('organization_name', '').strip() + contact.organization_type = request.form.get('organization_type', 'other') + contact.organization_address = request.form.get('organization_address', '').strip() or None + contact.organization_website = request.form.get('organization_website', '').strip() or None + contact.organization_logo_url = request.form.get('organization_logo_url', '').strip() or None + contact.project_name = request.form.get('project_name', '').strip() or None + contact.project_description = request.form.get('project_description', '').strip() or None + contact.source_url = request.form.get('source_url', '').strip() or None + contact.related_links = related_links + contact.tags = request.form.get('tags', '').strip() or None + contact.notes = request.form.get('notes', '').strip() or None + contact.updated_at = datetime.now() + + db.commit() + + flash(f'Kontakt {contact.full_name} został zaktualizowany.', 'success') + return redirect(url_for('contact_detail', contact_id=contact.id)) + + # GET - show form with existing data + return render_template('contacts/form.html', + contact=contact, + org_types=ExternalContact.ORGANIZATION_TYPES, + org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS) + + finally: + db.close() + + +@app.route('/kontakty//usun', methods=['POST']) +@login_required +def contact_delete(contact_id): + """ + Usuwanie kontaktu zewnętrznego (soft delete). + Może usunąć twórca kontaktu lub admin. + """ + from database import ExternalContact + + db = SessionLocal() + try: + contact = db.query(ExternalContact).filter( + ExternalContact.id == contact_id + ).first() + + if not contact: + flash('Kontakt nie został znaleziony.', 'error') + return redirect(url_for('contacts_list')) + + # Check permissions + if not current_user.is_admin and contact.created_by != current_user.id: + flash('Nie masz uprawnień do usunięcia tego kontaktu.', 'error') + return redirect(url_for('contact_detail', contact_id=contact_id)) + + # Soft delete + contact.is_active = False + contact.updated_at = datetime.now() + db.commit() + + flash(f'Kontakt {contact.full_name} został usunięty.', 'success') + return redirect(url_for('contacts_list')) + + finally: + db.close() + + # ============================================================ # HONEYPOT ENDPOINTS (trap for malicious bots) # ============================================================ diff --git a/database.py b/database.py index 4b0ac8d..c0ea49d 100644 --- a/database.py +++ b/database.py @@ -3112,6 +3112,117 @@ class AnnouncementRead(Base): return f"" +# ============================================================ +# EXTERNAL CONTACTS (Kontakty zewnętrzne) +# ============================================================ + +class ExternalContact(Base): + """ + Baza kontaktów zewnętrznych - urzędy, instytucje, partnerzy projektów. + Dostępna dla wszystkich zalogowanych członków Norda Biznes. + """ + __tablename__ = 'external_contacts' + + id = Column(Integer, primary_key=True) + + # Dane osobowe + first_name = Column(String(100), nullable=False) + last_name = Column(String(100), nullable=False) + position = Column(String(200)) # Stanowisko (opcjonalne) + photo_url = Column(String(500)) # Zdjęcie osoby (opcjonalne) + + # Dane kontaktowe + phone = Column(String(50)) + phone_secondary = Column(String(50)) # Drugi numer telefonu + email = Column(String(255)) + website = Column(String(500)) # Strona osobista/wizytówka + + # Social Media + linkedin_url = Column(String(500)) + facebook_url = Column(String(500)) + twitter_url = Column(String(500)) + + # Organizacja + organization_name = Column(String(300), nullable=False) + organization_type = Column(String(50), default='other') + # Typy: government (urząd), agency (agencja), company (firma), ngo (organizacja), university (uczelnia), other + + organization_address = Column(String(500)) + organization_website = Column(String(500)) + organization_logo_url = Column(String(500)) + + # Kontekst/Projekt + project_name = Column(String(300)) # Nazwa projektu (Tytani, EJ Choczewo, itp.) + project_description = Column(Text) # Krótki opis kontekstu + + # Źródło kontaktu + source_type = Column(String(50)) # announcement, forum_post, manual + source_id = Column(Integer) # ID ogłoszenia lub wpisu (opcjonalne) + source_url = Column(String(500)) # URL do źródła + + # Powiązane linki (artykuły, strony, dokumenty) - JSON array + # Format: [{"title": "Artykuł o...", "url": "https://...", "type": "article"}, ...] + related_links = Column(PG_JSONB, default=list) + + # Tagi do wyszukiwania + tags = Column(String(500)) # Tagi oddzielone przecinkami + + # Notatki + notes = Column(Text) + + # Audyt + created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL')) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + # Status + is_active = Column(Boolean, default=True) + is_verified = Column(Boolean, default=False) # Zweryfikowany przez admina/moderatora + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + + # Constants + ORGANIZATION_TYPES = ['government', 'agency', 'company', 'ngo', 'university', 'other'] + ORGANIZATION_TYPE_LABELS = { + 'government': 'Urząd', + 'agency': 'Agencja', + 'company': 'Firma', + 'ngo': 'Organizacja pozarządowa', + 'university': 'Uczelnia', + 'other': 'Inne' + } + SOURCE_TYPES = ['announcement', 'forum_post', 'manual'] + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + @property + def organization_type_label(self): + return self.ORGANIZATION_TYPE_LABELS.get(self.organization_type, self.organization_type) + + @property + def tags_list(self): + """Zwraca tagi jako listę.""" + if not self.tags: + return [] + return [tag.strip() for tag in self.tags.split(',') if tag.strip()] + + @property + def has_social_media(self): + """Sprawdza czy kontakt ma jakiekolwiek social media.""" + return bool(self.linkedin_url or self.facebook_url or self.twitter_url) + + @property + def has_contact_info(self): + """Sprawdza czy kontakt ma dane kontaktowe.""" + return bool(self.phone or self.email or self.website) + + def __repr__(self): + return f"" + + # ============================================================ # ZOPK MILESTONES (Timeline) # ============================================================ diff --git a/database/migrations/020_external_contacts.sql b/database/migrations/020_external_contacts.sql new file mode 100644 index 0000000..2f30196 --- /dev/null +++ b/database/migrations/020_external_contacts.sql @@ -0,0 +1,96 @@ +-- ============================================================ +-- Migration: 020_external_contacts.sql +-- Description: Baza kontaktów zewnętrznych (urzędy, instytucje, partnerzy) +-- Author: Maciej Pienczyn +-- Date: 2026-01-27 +-- ============================================================ + +-- Tabela kontaktów zewnętrznych +CREATE TABLE IF NOT EXISTS external_contacts ( + id SERIAL PRIMARY KEY, + + -- Dane osobowe + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + position VARCHAR(200), -- Stanowisko (opcjonalne) + photo_url VARCHAR(500), -- Zdjęcie osoby (opcjonalne) + + -- Dane kontaktowe + phone VARCHAR(50), + phone_secondary VARCHAR(50), -- Drugi numer telefonu + email VARCHAR(255), + website VARCHAR(500), -- Strona osobista/wizytówka + + -- Social Media + linkedin_url VARCHAR(500), + facebook_url VARCHAR(500), + twitter_url VARCHAR(500), + + -- Organizacja + organization_name VARCHAR(300) NOT NULL, + organization_type VARCHAR(50) DEFAULT 'other', + -- Typy: government (urząd), agency (agencja), company (firma), ngo (organizacja), university (uczelnia), other + + organization_address VARCHAR(500), + organization_website VARCHAR(500), + organization_logo_url VARCHAR(500), + + -- Kontekst/Projekt + project_name VARCHAR(300), -- Nazwa projektu (Tytani, EJ Choczewo, itp.) + project_description TEXT, -- Krótki opis kontekstu + + -- Źródło kontaktu + source_type VARCHAR(50), -- announcement, forum_post, manual + source_id INTEGER, -- ID ogłoszenia lub wpisu (opcjonalne) + source_url VARCHAR(500), -- URL do źródła + + -- Powiązane linki (artykuły, strony, dokumenty) - JSON array + -- Format: [{"title": "Artykuł o...", "url": "https://...", "type": "article"}, ...] + related_links JSONB DEFAULT '[]', + + -- Tagi do wyszukiwania + tags VARCHAR(500), -- Tagi oddzielone przecinkami + + -- Notatki + notes TEXT, + + -- Audyt + created_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Status + is_active BOOLEAN DEFAULT TRUE, + is_verified BOOLEAN DEFAULT FALSE -- Zweryfikowany przez admina/moderatora +); + +-- Indeksy dla szybkiego wyszukiwania +CREATE INDEX IF NOT EXISTS idx_external_contacts_name ON external_contacts(last_name, first_name); +CREATE INDEX IF NOT EXISTS idx_external_contacts_organization ON external_contacts(organization_name); +CREATE INDEX IF NOT EXISTS idx_external_contacts_org_type ON external_contacts(organization_type); +CREATE INDEX IF NOT EXISTS idx_external_contacts_project ON external_contacts(project_name); +CREATE INDEX IF NOT EXISTS idx_external_contacts_active ON external_contacts(is_active); + +-- Full-text search index (PostgreSQL) +CREATE INDEX IF NOT EXISTS idx_external_contacts_search ON external_contacts +USING gin(to_tsvector('polish', + COALESCE(first_name, '') || ' ' || + COALESCE(last_name, '') || ' ' || + COALESCE(organization_name, '') || ' ' || + COALESCE(project_name, '') || ' ' || + COALESCE(tags, '') +)); + +-- Indeks na JSONB dla related_links +CREATE INDEX IF NOT EXISTS idx_external_contacts_links ON external_contacts USING gin(related_links); + +-- Uprawnienia +GRANT ALL ON TABLE external_contacts TO nordabiz_app; +GRANT USAGE, SELECT ON SEQUENCE external_contacts_id_seq TO nordabiz_app; + +-- Komentarze +COMMENT ON TABLE external_contacts IS 'Baza kontaktów zewnętrznych - urzędy, instytucje, partnerzy projektów'; +COMMENT ON COLUMN external_contacts.organization_type IS 'Typ: government, agency, company, ngo, university, other'; +COMMENT ON COLUMN external_contacts.source_type IS 'Źródło: announcement, forum_post, manual'; +COMMENT ON COLUMN external_contacts.tags IS 'Tagi do wyszukiwania, oddzielone przecinkami'; +COMMENT ON COLUMN external_contacts.related_links IS 'JSON array z linkami: [{"title": "...", "url": "...", "type": "article|document|video"}]'; diff --git a/templates/base.html b/templates/base.html index edf5f05..e05e6b8 100755 --- a/templates/base.html +++ b/templates/base.html @@ -960,6 +960,7 @@
  • Tablica B2B
  • NordaGPT
  • ZOP Kaszubia
  • +
  • Kontakty zewnętrzne
  • Mapa Powiązań
  • diff --git a/templates/chat.html b/templates/chat.html index ba4cec2..e2964b6 100755 --- a/templates/chat.html +++ b/templates/chat.html @@ -847,7 +847,7 @@
    Listopad 2025
    Uruchomienie NordaGPT -

    Premiera asystenta AI dla członków Norda Biznes. Integracja z bazą 80 firm.

    +

    Premiera asystenta AI dla członków Norda Biznes. Integracja z bazą 111 firm.

    Premiera
    diff --git a/templates/contacts/detail.html b/templates/contacts/detail.html new file mode 100644 index 0000000..d8fdc88 --- /dev/null +++ b/templates/contacts/detail.html @@ -0,0 +1,732 @@ +{% 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 %} + +{% endblock %} + +{% block content %} +
    + + ← Powrot do listy kontaktow + + +
    + +
    +
    +
    + {% if contact.photo_url %} + {{ contact.full_name }} + {% else %} + {{ contact.first_name[0]|upper }} + {% endif %} +
    +
    {{ contact.full_name }}
    + {% if contact.position %} +
    {{ contact.position }}
    + {% endif %} +
    {{ contact.organization_name }}
    + + {{ org_type_labels.get(contact.organization_type, contact.organization_type) }} + + {% if contact.is_verified %} +
    + ✓ Zweryfikowany +
    + {% endif %} +
    + +
    + + {% if contact.has_contact_info or contact.phone_secondary %} +
    +

    📞 Dane kontaktowe

    +
    + {% if contact.phone %} +
    + 📞 +
    +
    Telefon
    + +
    +
    + {% endif %} + {% if contact.phone_secondary %} +
    + 📱 +
    +
    Telefon dodatkowy
    + +
    +
    + {% endif %} + {% if contact.email %} +
    + +
    +
    Email
    + +
    +
    + {% endif %} + {% if contact.website %} +
    + 🌐 +
    +
    Strona WWW
    + +
    +
    + {% endif %} +
    +
    + {% endif %} + + + {% if contact.has_social_media %} +
    +

    👥 Media spolecznosciowe

    + +
    + {% endif %} + + +
    +

    🏢 Organizacja

    +
    +
    + 🏢 +
    +
    Nazwa
    +
    {{ contact.organization_name }}
    +
    +
    + {% if contact.organization_address %} +
    + 📍 +
    +
    Adres
    +
    {{ contact.organization_address }}
    +
    +
    + {% endif %} + {% if contact.organization_website %} +
    + 🌐 +
    +
    Strona organizacji
    + +
    +
    + {% endif %} +
    +
    + + + {% if contact.project_name %} +
    +

    🚀 Projekt

    +
    +
    {{ contact.project_name }}
    + {% if contact.project_description %} +
    {{ contact.project_description }}
    + {% endif %} +
    +
    + {% endif %} + + + {% if contact.related_links and contact.related_links|length > 0 %} + + {% endif %} + + + {% if contact.tags_list %} +
    +

    🏷 Tagi

    +
    + {% for tag in contact.tags_list %} + {{ tag }} + {% endfor %} +
    +
    + {% endif %} + + + {% if contact.notes %} +
    +

    📝 Notatki

    +
    {{ contact.notes }}
    +
    + {% endif %} + + + {% if contact.source_url %} +
    +

    📖 Zrodlo

    + +
    + {% endif %} + +
    + 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 %} +
    +
    +
    + + + +
    +
    +{% endblock %} diff --git a/templates/contacts/form.html b/templates/contacts/form.html new file mode 100644 index 0000000..7618097 --- /dev/null +++ b/templates/contacts/form.html @@ -0,0 +1,577 @@ +{% extends "base.html" %} + +{% block title %}{% if contact %}Edytuj kontakt{% else %}Dodaj kontakt{% endif %} - Norda Biznes Hub{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
    +
    + + ← Powrot do listy kontaktow + + +
    +

    {% if contact %}✎ Edytuj kontakt{% else %}+ Dodaj nowy kontakt{% endif %}

    +

    Kontakty zewnetrzne - osoby z urzedow, instytucji, agencji i partnerow projektow.

    +
    + +
    + + + +
    +
    +

    👤 Dane osobowe

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    Opcjonalnie - link do zdjecia profilowego
    +
    +
    +
    +
    + + +
    +
    +

    📞 Dane kontaktowe

    +
    +
    +
    +
    + +
    + 📞 + +
    +
    +
    + +
    + 📱 + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + 🌐 + +
    +
    +
    +
    +
    + + +
    +
    +

    👥 Media spolecznosciowe

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    🏢 Organizacja

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    🚀 Projekt / Kontekst

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    🔗 Powiazane materialy

    +
    +
    +
    + + +
    Dodaj linki do artykulow, dokumentow lub filmow zwiazanych z ta osoba
    +
    + +
    +
    + + +
    +
    +

    📝 Dodatkowe informacje

    +
    +
    +
    +
    + + +
    Tagi pomagaja w wyszukiwaniu - oddziel przecinkami
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    + + Anuluj + +
    + +
    +
    +
    +
    +
    +{% 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 = ` + + + + + `; + 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 %} diff --git a/templates/contacts/list.html b/templates/contacts/list.html new file mode 100644 index 0000000..58e4db7 --- /dev/null +++ b/templates/contacts/list.html @@ -0,0 +1,474 @@ +{% 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 %} + +{% endblock %} + +{% block content %} +
    +
    +

    👥 Kontakty zewnetrzne

    + + + Dodaj kontakt + +
    + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + +
    + Znaleziono: {{ total }} kontaktow + {% if search or org_type or project %} + Wyczysc filtry + {% endif %} +
    + + {% if contacts %} +
    + {% for contact in contacts %} +
    +
    +
    + {% if contact.photo_url %} + {{ contact.full_name }} + {% else %} + {{ contact.first_name[0]|upper }} + {% endif %} +
    +
    + + {% if contact.position %} +
    {{ contact.position }}
    + {% endif %} +
    + {{ contact.organization_name }} + + {{ org_type_labels.get(contact.organization_type, contact.organization_type) }} + +
    +
    +
    +
    +
    + {% if contact.phone %} + + {% endif %} + {% if contact.email %} + + {% endif %} +
    + + {% if contact.has_social_media %} + + {% endif %} + + {% if contact.project_name %} +
    + Projekt: {{ contact.project_name }} +
    + {% endif %} +
    +
    + {% endfor %} +
    + + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
    +
    📋
    +

    Brak kontaktow

    +

    + {% if search or org_type or project %} + Nie znaleziono kontaktow pasujacych do podanych kryteriow. + {% else %} + Baza kontaktow zewnetrznych jest pusta. Dodaj pierwszy kontakt! + {% endif %} +

    + + + Dodaj pierwszy kontakt + +
    + {% endif %} +
    +{% endblock %}