diff --git a/blueprints/public/routes_company_edit.py b/blueprints/public/routes_company_edit.py index a4f548e..0123f88 100644 --- a/blueprints/public/routes_company_edit.py +++ b/blueprints/public/routes_company_edit.py @@ -9,7 +9,7 @@ from flask import render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from blueprints.public import bp from sqlalchemy import or_ -from database import SessionLocal, Company, CompanyContact, CompanySocialMedia, Category +from database import SessionLocal, Company, CompanyContact, CompanySocialMedia, CompanyWebsite, Category from utils.helpers import sanitize_input, sanitize_html, validate_email, ensure_url from datetime import datetime import logging @@ -53,6 +53,10 @@ def company_edit(company_id=None): company_id=company.id ).all() + company_websites = db.query(CompanyWebsite).filter_by( + company_id=company.id + ).order_by(CompanyWebsite.is_primary.desc()).all() + permissions = { 'description': current_user.can_edit_company_field('description', company_id=company.id), 'services': current_user.can_edit_company_field('services', company_id=company.id), @@ -69,6 +73,7 @@ def company_edit(company_id=None): contacts=editable_contacts, all_contacts=contacts, social_media=social_media, + company_websites=company_websites, permissions=permissions, ) finally: @@ -171,8 +176,7 @@ def _save_services(company): def _save_contacts(db, company): """Save contacts tab fields.""" - website_raw = sanitize_input(request.form.get('website', ''), max_length=500) - company.website = ensure_url(website_raw) if website_raw else None + _save_websites(db, company) email_raw = sanitize_input(request.form.get('email', ''), max_length=255) if email_raw: @@ -252,3 +256,54 @@ def _save_social_media(db, company): source='manual_edit', verified_at=datetime.now(), )) + + +def _save_websites(db, company): + """Save multiple website URLs from the contacts tab.""" + # Delete existing editable websites + db.query(CompanyWebsite).filter( + CompanyWebsite.company_id == company.id, + or_( + CompanyWebsite.source.in_(['manual_edit', 'manual', 'migration']), + CompanyWebsite.source.is_(None) + ) + ).delete(synchronize_session='fetch') + + website_urls = request.form.getlist('website_urls[]') + website_labels = request.form.getlist('website_labels[]') + primary_idx_raw = request.form.get('website_primary', '0') + try: + primary_idx = int(primary_idx_raw) + except (ValueError, TypeError): + primary_idx = 0 + + added = 0 + primary_url = None + for i, url_raw in enumerate(website_urls): + if added >= 5: + break + url_raw = sanitize_input(url_raw, max_length=500) + if not url_raw: + continue + url = ensure_url(url_raw) + label = sanitize_input(website_labels[i], max_length=100) if i < len(website_labels) else '' + is_primary = (i == primary_idx) + if is_primary: + primary_url = url + db.add(CompanyWebsite( + company_id=company.id, + url=url, + label=label or None, + is_primary=is_primary, + source='manual_edit', + )) + added += 1 + + # Sync company.website with primary for backward compatibility + if primary_url: + company.website = primary_url + elif added > 0: + # No explicit primary — first one becomes primary + company.website = ensure_url(sanitize_input(website_urls[0], max_length=500)) + else: + company.website = None diff --git a/database.py b/database.py index 31cfbc3..54d4a79 100644 --- a/database.py +++ b/database.py @@ -850,6 +850,11 @@ class Company(Base): ai_insights = relationship('CompanyAIInsights', back_populates='company', uselist=False) ai_enrichment_proposals = relationship('AiEnrichmentProposal', back_populates='company', cascade='all, delete-orphan') + # Multiple websites + websites = relationship('CompanyWebsite', back_populates='company', + cascade='all, delete-orphan', + order_by='CompanyWebsite.is_primary.desc()') + class Service(Base): """Services offered by companies""" @@ -2441,6 +2446,24 @@ class CompanySocialMedia(Base): ) +class CompanyWebsite(Base): + """Multiple website URLs for companies""" + __tablename__ = 'company_websites' + + id = Column(Integer, primary_key=True) + company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) + url = Column(String(500), nullable=False) + label = Column(String(100)) # optional: "Sklep", "Strona główna" + is_primary = Column(Boolean, default=False) + source = Column(String(100)) # manual_edit, migration, website_scrape + is_valid = Column(Boolean, default=True) + last_checked_at = Column(DateTime) + check_status = Column(String(50)) # ok, 404, redirect, blocked + created_at = Column(DateTime, default=datetime.now) + + company = relationship('Company', back_populates='websites') + + class CompanyRecommendation(Base): """Peer recommendations between NORDA BIZNES members""" __tablename__ = 'company_recommendations' diff --git a/database/migrations/067_company_websites.sql b/database/migrations/067_company_websites.sql new file mode 100644 index 0000000..5e52e46 --- /dev/null +++ b/database/migrations/067_company_websites.sql @@ -0,0 +1,28 @@ +-- Migration 067: Add company_websites table for multiple website URLs per company +-- Date: 2026-02-17 + +CREATE TABLE IF NOT EXISTS company_websites ( + id SERIAL PRIMARY KEY, + company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + url VARCHAR(500) NOT NULL, + label VARCHAR(100), + is_primary BOOLEAN DEFAULT FALSE, + source VARCHAR(100), + is_valid BOOLEAN DEFAULT TRUE, + last_checked_at TIMESTAMP, + check_status VARCHAR(50), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_company_websites_company_id ON company_websites(company_id); + +-- Grant permissions +GRANT ALL ON TABLE company_websites TO nordabiz_app; +GRANT USAGE, SELECT ON SEQUENCE company_websites_id_seq TO nordabiz_app; + +-- Migrate existing data: each company.website → company_websites row with is_primary=True +INSERT INTO company_websites (company_id, url, is_primary, source, created_at) +SELECT id, website, TRUE, 'migration', NOW() +FROM companies +WHERE website IS NOT NULL AND website != '' +ON CONFLICT DO NOTHING; diff --git a/templates/company_detail.html b/templates/company_detail.html index e6bbc9e..ddb2bcd 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -698,7 +698,17 @@
- {% if company.website %} + {% if company.websites %} + {% for w in company.websites %} + + + + + + {{ w.label if w.label else ('WWW' if w.is_primary else w.url|replace('https://', '')|replace('http://', '')|replace('www.', '')|truncate(20, True)) }} + + {% endfor %} + {% elif company.website %} @@ -2486,6 +2496,37 @@
+ + {% if company.websites %} + {% set extra_websites = company.websites|rejectattr('is_primary')|list %} + {% if extra_websites %} + {% for w in extra_websites %} +
+
+ + + + +
+
+ {% if w.label %}
{{ w.label }}
{% endif %} + + {{ w.url|replace('https://', '')|replace('http://', '')|replace('www.', '') }} + + + + +
+ + Odwiedź + +
+ {% endfor %} + {% endif %} + {% endif %} +
diff --git a/templates/company_detail_safe.html b/templates/company_detail_safe.html index becc2bc..d68d6bc 100755 --- a/templates/company_detail_safe.html +++ b/templates/company_detail_safe.html @@ -203,7 +203,19 @@ {% endif %}
- {% if company.website %} + {% if company.websites %} + {% for w in company.websites %} + + {% endfor %} + {% elif company.website %}
diff --git a/templates/company_edit.html b/templates/company_edit.html index f9d54b9..d4ee39e 100644 --- a/templates/company_edit.html +++ b/templates/company_edit.html @@ -278,6 +278,22 @@ /* Social platform icon hints */ .social-row .social-platform-select { font-weight: 500; } + /* Website primary radio */ + .website-primary-label { + display: flex; + align-items: center; + gap: 4px; + flex: 0 0 auto; + font-size: var(--font-size-sm); + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; + } + .website-primary-label input[type="radio"]:checked + span { + color: var(--primary, #2E4872); + font-weight: 600; + } + /* Form actions (sticky bottom) */ .ce-actions { display: flex; @@ -462,11 +478,41 @@
Dane kontaktowe -
-
- - +
+ +
+ {% for w in company_websites %} + + {% endfor %} + {% if not company_websites %} + {% if company.website %} + + {% endif %} + {% endif %}
+ +
+ +
@@ -691,10 +737,60 @@ return; } } - var websiteField = document.getElementById('website'); - if (websiteField && websiteField.value.trim() && !websiteField.value.match(/^https?:\/\//)) { - websiteField.value = 'https://' + websiteField.value.trim(); - } + // Auto-prefix https:// for all website URL inputs + document.querySelectorAll('#websiteList input[name="website_urls[]"]').forEach(function(f) { + if (f.value.trim() && !f.value.match(/^https?:\/\//)) { + f.value = 'https://' + f.value.trim(); + } + }); }); })(); + +// Website list management +function removeWebsiteRow(btn) { + btn.closest('.website-row').remove(); + reindexWebsiteRadios(); + toggleWebsiteBtn(); +} + +function reindexWebsiteRadios() { + var rows = document.querySelectorAll('#websiteList .website-row'); + var hadChecked = false; + rows.forEach(function(row, i) { + var radio = row.querySelector('input[type="radio"]'); + radio.value = i; + if (radio.checked) hadChecked = true; + }); + if (!hadChecked && rows.length > 0) { + rows[0].querySelector('input[type="radio"]').checked = true; + } +} + +function toggleWebsiteBtn() { + var btn = document.getElementById('addWebsiteBtn'); + if (btn) { + btn.style.display = document.querySelectorAll('#websiteList .website-row').length >= 5 ? 'none' : ''; + } +} + +(function() { + var addBtn = document.getElementById('addWebsiteBtn'); + if (addBtn) { + addBtn.addEventListener('click', function() { + var list = document.getElementById('websiteList'); + var idx = list.querySelectorAll('.website-row').length; + if (idx >= 5) return; + var row = document.createElement('div'); + row.className = 'social-row website-row'; + row.innerHTML = '' + + '' + + '' + + ''; + list.appendChild(row); + if (idx === 0) row.querySelector('input[type="radio"]').checked = true; + toggleWebsiteBtn(); + }); + toggleWebsiteBtn(); + } +})(); {% endblock %}