feat: Add website_type with color-coded buttons and tooltips
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Six types: website (blue), store (green), booking (purple), blog (orange),
portfolio (pink), other (gray). Each type has unique icon, color in contact
bar and banner section, and tooltip with site description.
Form edit adds type selector dropdown per website row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-17 07:59:44 +01:00
parent b877773b69
commit 9c24c8bbab
5 changed files with 123 additions and 53 deletions

View File

@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
EMPLOYEE_COUNT_WHITELIST = ['1-5', '6-10', '11-25', '26-50', '51-100', '101-250', '250+', '']
VALID_SOCIAL_PLATFORMS = ['facebook', 'linkedin', 'instagram', 'youtube', 'twitter', 'tiktok']
VALID_WEBSITE_TYPES = ['website', 'store', 'booking', 'blog', 'portfolio', 'other']
EDITABLE_SOURCES = [None, 'manual_edit', 'manual']
@ -271,6 +272,7 @@ def _save_websites(db, company):
website_urls = request.form.getlist('website_urls[]')
website_labels = request.form.getlist('website_labels[]')
website_types = request.form.getlist('website_types[]')
primary_idx_raw = request.form.get('website_primary', '0')
try:
primary_idx = int(primary_idx_raw)
@ -287,6 +289,9 @@ def _save_websites(db, company):
continue
url = ensure_url(url_raw)
label = sanitize_input(website_labels[i], max_length=100) if i < len(website_labels) else ''
wtype = sanitize_input(website_types[i], max_length=20) if i < len(website_types) else 'website'
if wtype not in VALID_WEBSITE_TYPES:
wtype = 'website'
is_primary = (i == primary_idx)
if is_primary:
primary_url = url
@ -294,6 +299,7 @@ def _save_websites(db, company):
company_id=company.id,
url=url,
label=label or None,
website_type=wtype,
is_primary=is_primary,
source='manual_edit',
))

View File

@ -2454,6 +2454,7 @@ class CompanyWebsite(Base):
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"
website_type = Column(String(20), default='website') # website, store, booking, blog, portfolio, other
is_primary = Column(Boolean, default=False)
source = Column(String(100)) # manual_edit, migration, website_scrape
is_valid = Column(Boolean, default=True)

View File

@ -0,0 +1,10 @@
-- Migration 067c: Add website_type column to company_websites
-- Date: 2026-02-17
ALTER TABLE company_websites ADD COLUMN IF NOT EXISTS website_type VARCHAR(20) DEFAULT 'website';
-- Auto-detect store type from URL patterns
UPDATE company_websites
SET website_type = 'store'
WHERE website_type = 'website'
AND (url ILIKE '%sklep%' OR url ILIKE '%shop%' OR url ILIKE '%store%' OR url ILIKE '%hurt%');

View File

@ -544,6 +544,17 @@
.contact-bar-item.social-tiktok { color: #000000; border-color: #000000; }
.contact-bar-item.social-tiktok:hover { background: #000000; color: white; }
/* Website type colors (driven by --wb-color CSS variable) */
.contact-bar-item.contact-bar-website {
color: var(--wb-color, #3b82f6);
border-color: var(--wb-color, #3b82f6);
}
.contact-bar-item.contact-bar-website:hover {
background: var(--wb-color, #3b82f6);
color: white;
border-color: var(--wb-color, #3b82f6);
}
/* GBP Audit link - styled as action button */
.contact-bar-item.gbp-audit { color: #4285f4; border-color: #4285f4; background: rgba(66, 133, 244, 0.05); }
.contact-bar-item.gbp-audit:hover { background: #4285f4; color: white; }
@ -697,19 +708,39 @@
{% endif %}
<!-- PASEK KONTAKTOWY - szybki dostep -->
{# Website type config: color, label, tooltip, icon #}
{% set wtype_config = {
'website': {'color': '#3b82f6', 'icon': 'globe', 'default_label': 'WWW', 'tooltip': 'Strona internetowa firmy'},
'store': {'color': '#10b981', 'icon': 'cart', 'default_label': 'Sklep', 'tooltip': 'Sklep internetowy'},
'booking': {'color': '#8b5cf6', 'icon': 'calendar', 'default_label': 'Rezerwacje', 'tooltip': 'System rezerwacji'},
'blog': {'color': '#f59e0b', 'icon': 'pencil', 'default_label': 'Blog', 'tooltip': 'Blog / Aktualności'},
'portfolio': {'color': '#ec4899', 'icon': 'image', 'default_label': 'Portfolio', 'tooltip': 'Portfolio'},
'other': {'color': '#64748b', 'icon': 'globe', 'default_label': 'WWW', 'tooltip': 'Strona internetowa'}
} %}
<div class="contact-bar">
{% if company.websites %}
{% for w in company.websites %}
<a href="{{ w.url }}" class="contact-bar-item" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
<span>{{ w.label if w.label else ('WWW' if w.is_primary else w.url|replace('https://', '')|replace('http://', '')|replace('www.', '')|truncate(20, True)) }}</span>
{% set cfg = wtype_config.get(w.website_type or 'website', wtype_config['website']) %}
<a href="{{ w.url }}" class="contact-bar-item contact-bar-website" target="_blank" rel="noopener noreferrer"
style="--wb-color: {{ cfg.color }};"
title="{{ w.label ~ ' — ' ~ w.url|replace('https://', '')|replace('http://', '')|replace('www.', '') if w.label else cfg.tooltip ~ ' — ' ~ w.url|replace('https://', '')|replace('http://', '')|replace('www.', '') }}">
{% if cfg.icon == 'cart' %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg>
{% elif cfg.icon == 'calendar' %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
{% elif cfg.icon == 'pencil' %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
{% elif cfg.icon == 'image' %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
{% else %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
{% endif %}
<span>{{ w.label if w.label else cfg.default_label }}</span>
</a>
{% endfor %}
{% elif company.website %}
<a href="{{ company.website }}" class="contact-bar-item" target="_blank" rel="noopener noreferrer">
<a href="{{ company.website }}" class="contact-bar-item" target="_blank" rel="noopener noreferrer"
title="Strona internetowa firmy — {{ company.website|replace('https://', '')|replace('http://', '')|replace('www.', '') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
@ -2474,22 +2505,52 @@
<div class="company-section">
<h2 class="section-title">Strona WWW</h2>
<!-- Primary website - prominent display -->
<div style="margin-bottom: var(--spacing-md); padding: var(--spacing-lg); background: linear-gradient(135deg, #3b82f6, #1d4ed8); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-md);">
<div style="width: 56px; height: 56px; border-radius: 12px; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center;">
<svg width="28" height="28" fill="white" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="none" stroke="white" stroke-width="2"/>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" fill="none" stroke="white" stroke-width="2"/>
</svg>
<!-- Website banners — all sites, primary first (from relationship order) -->
{% for w in (company.websites if company.websites else []) %}
{% set cfg = wtype_config.get(w.website_type or 'website', wtype_config['website']) %}
{% set banner_label = cfg.tooltip ~ (' — ' ~ w.label if w.label else '') %}
<div style="margin-bottom: var(--spacing-md); padding: {{ 'var(--spacing-lg)' if w.is_primary else 'var(--spacing-md) var(--spacing-lg)' }}; background: linear-gradient(135deg, {{ cfg.color }}, {{ cfg.color }}dd); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-md);">
<div style="width: {{ '56px' if w.is_primary else '40px' }}; height: {{ '56px' if w.is_primary else '40px' }}; border-radius: {{ '12px' if w.is_primary else '10px' }}; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center;">
{% if cfg.icon == 'cart' %}
<svg width="{{ '28' if w.is_primary else '20' }}" height="{{ '28' if w.is_primary else '20' }}" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg>
{% elif cfg.icon == 'calendar' %}
<svg width="{{ '28' if w.is_primary else '20' }}" height="{{ '28' if w.is_primary else '20' }}" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
{% elif cfg.icon == 'pencil' %}
<svg width="{{ '28' if w.is_primary else '20' }}" height="{{ '28' if w.is_primary else '20' }}" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
{% elif cfg.icon == 'image' %}
<svg width="{{ '28' if w.is_primary else '20' }}" height="{{ '28' if w.is_primary else '20' }}" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
{% else %}
<svg width="{{ '28' if w.is_primary else '20' }}" height="{{ '28' if w.is_primary else '20' }}" fill="white" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="white" stroke-width="2"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" fill="none" stroke="white" stroke-width="2"/></svg>
{% endif %}
</div>
<div style="flex: 1;">
<div style="font-size: var(--font-size-sm); color: rgba(255,255,255,0.8); margin-bottom: 4px;">Adres strony{% if primary_website and primary_website.label %} &mdash; {{ primary_website.label }}{% endif %}</div>
<div style="font-size: var(--font-size-sm); color: rgba(255,255,255,0.8); margin-bottom: 4px;">{{ banner_label }}</div>
<a href="{{ w.url|ensure_url }}" target="_blank" rel="noopener noreferrer"
style="font-size: {{ 'var(--font-size-xl)' if w.is_primary else 'var(--font-size-lg)' }}; font-weight: 600; color: white; text-decoration: none; display: flex; align-items: center; gap: var(--spacing-sm);">
{{ w.url|replace('https://', '')|replace('http://', '')|replace('www.', '') }}
<svg width="{{ '20' if w.is_primary else '16' }}" height="{{ '20' if w.is_primary else '16' }}" fill="white" viewBox="0 0 24 24" style="opacity: 0.8;">
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
</a>
</div>
<a href="{{ w.url|ensure_url }}" target="_blank" rel="noopener noreferrer"
style="padding: var(--spacing-sm) var(--spacing-lg); background: white; color: {{ cfg.color }}; border-radius: var(--radius); text-decoration: none; font-weight: 600; font-size: var(--font-size-sm);">
{{ 'Odwiedź stronę' if w.is_primary else 'Odwiedź' }}
</a>
</div>
{% endfor %}
{% if not company.websites and primary_url %}
<!-- Fallback for companies without relationship data -->
<div style="margin-bottom: var(--spacing-md); padding: var(--spacing-lg); background: linear-gradient(135deg, #3b82f6, #1d4ed8); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-md);">
<div style="width: 56px; height: 56px; border-radius: 12px; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center;">
<svg width="28" height="28" fill="white" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="white" stroke-width="2"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" fill="none" stroke="white" stroke-width="2"/></svg>
</div>
<div style="flex: 1;">
<div style="font-size: var(--font-size-sm); color: rgba(255,255,255,0.8); margin-bottom: 4px;">Adres strony</div>
<a href="{{ primary_url|ensure_url }}" target="_blank" rel="noopener noreferrer"
style="font-size: var(--font-size-xl); font-weight: 600; color: white; text-decoration: none; display: flex; align-items: center; gap: var(--spacing-sm);">
{{ primary_url|replace('https://', '')|replace('http://', '')|replace('www.', '') }}
<svg width="20" height="20" fill="white" viewBox="0 0 24 24" style="opacity: 0.8;">
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
<svg width="20" height="20" fill="white" viewBox="0 0 24 24" style="opacity: 0.8;"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
</a>
</div>
<a href="{{ primary_url|ensure_url }}" target="_blank" rel="noopener noreferrer"
@ -2497,34 +2558,6 @@
Odwiedź stronę
</a>
</div>
<!-- Additional websites -->
{% if company.websites %}
{% set extra_websites = company.websites|rejectattr('is_primary')|list %}
{% for w in extra_websites %}
<div style="margin-bottom: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); background: linear-gradient(135deg, #64748b, #475569); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-md);">
<div style="width: 40px; height: 40px; border-radius: 10px; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center;">
<svg width="20" height="20" fill="white" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="none" stroke="white" stroke-width="2"/>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" fill="none" stroke="white" stroke-width="2"/>
</svg>
</div>
<div style="flex: 1;">
{% if w.label %}<div style="font-size: var(--font-size-xs); color: rgba(255,255,255,0.7); margin-bottom: 2px;">{{ w.label }}</div>{% endif %}
<a href="{{ w.url|ensure_url }}" target="_blank" rel="noopener noreferrer"
style="font-size: var(--font-size-lg); font-weight: 600; color: white; text-decoration: none; display: flex; align-items: center; gap: var(--spacing-sm);">
{{ w.url|replace('https://', '')|replace('http://', '')|replace('www.', '') }}
<svg width="16" height="16" fill="white" viewBox="0 0 24 24" style="opacity: 0.8;">
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
</a>
</div>
<a href="{{ w.url|ensure_url }}" target="_blank" rel="noopener noreferrer"
style="padding: var(--spacing-xs) var(--spacing-md); background: white; color: #475569; border-radius: var(--radius); text-decoration: none; font-weight: 600; font-size: var(--font-size-sm);">
Odwiedź
</a>
</div>
{% endfor %}
{% endif %}
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-lg);">

View File

@ -278,6 +278,9 @@
/* Social platform icon hints */
.social-row .social-platform-select { font-weight: 500; }
/* Website type select */
.website-type-select { flex: 0 0 140px; font-weight: 500; }
/* Website primary radio */
.website-primary-label {
display: flex;
@ -483,11 +486,19 @@
<div id="websiteList">
{% for w in company_websites %}
<div class="social-row website-row">
<select name="website_types[]" class="form-input website-type-select">
<option value="website" {% if w.website_type == 'website' or not w.website_type %}selected{% endif %}>Strona firmowa</option>
<option value="store" {% if w.website_type == 'store' %}selected{% endif %}>Sklep</option>
<option value="booking" {% if w.website_type == 'booking' %}selected{% endif %}>Rezerwacje</option>
<option value="blog" {% if w.website_type == 'blog' %}selected{% endif %}>Blog</option>
<option value="portfolio" {% if w.website_type == 'portfolio' %}selected{% endif %}>Portfolio</option>
<option value="other" {% if w.website_type == 'other' %}selected{% endif %}>Inna</option>
</select>
<input type="url" name="website_urls[]" class="form-input social-url-input" value="{{ w.url }}" placeholder="https://www.twojafirma.pl">
<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 140px;" value="{{ w.label or '' }}" placeholder="Etykieta (np. Sklep)">
<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 120px;" value="{{ w.label or '' }}" placeholder="Etykieta">
<label class="website-primary-label" title="Strona główna">
<input type="radio" name="website_primary" value="{{ loop.index0 }}" {% if w.is_primary %}checked{% endif %}>
<span>Główna</span>
<span>.</span>
</label>
<button type="button" class="btn-remove" onclick="removeWebsiteRow(this)" title="Usuń">&#x2715;</button>
</div>
@ -495,11 +506,19 @@
{% if not company_websites %}
{% if company.website %}
<div class="social-row website-row">
<select name="website_types[]" class="form-input website-type-select">
<option value="website" selected>Strona firmowa</option>
<option value="store">Sklep</option>
<option value="booking">Rezerwacje</option>
<option value="blog">Blog</option>
<option value="portfolio">Portfolio</option>
<option value="other">Inna</option>
</select>
<input type="url" name="website_urls[]" class="form-input social-url-input" value="{{ company.website }}" placeholder="https://www.twojafirma.pl">
<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 140px;" value="" placeholder="Etykieta (np. Sklep)">
<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 120px;" value="" placeholder="Etykieta">
<label class="website-primary-label" title="Strona główna">
<input type="radio" name="website_primary" value="0" checked>
<span>Główna</span>
<span>.</span>
</label>
<button type="button" class="btn-remove" onclick="removeWebsiteRow(this)" title="Usuń">&#x2715;</button>
</div>
@ -782,9 +801,10 @@ function toggleWebsiteBtn() {
if (idx >= 5) return;
var row = document.createElement('div');
row.className = 'social-row website-row';
row.innerHTML = '<input type="url" name="website_urls[]" class="form-input social-url-input" value="" placeholder="https://www.twojafirma.pl">'
+ '<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 140px;" value="" placeholder="Etykieta (np. Sklep)">'
+ '<label class="website-primary-label" title="Strona główna"><input type="radio" name="website_primary" value="' + idx + '"><span>Główna</span></label>'
row.innerHTML = '<select name="website_types[]" class="form-input website-type-select"><option value="website">Strona firmowa</option><option value="store">Sklep</option><option value="booking">Rezerwacje</option><option value="blog">Blog</option><option value="portfolio">Portfolio</option><option value="other">Inna</option></select>'
+ '<input type="url" name="website_urls[]" class="form-input social-url-input" value="" placeholder="https://www.twojafirma.pl">'
+ '<input type="text" name="website_labels[]" class="form-input" style="flex: 0 0 120px;" value="" placeholder="Etykieta">'
+ '<label class="website-primary-label" title="Strona główna"><input type="radio" name="website_primary" value="' + idx + '"><span>Gł.</span></label>'
+ '<button type="button" class="btn-remove" onclick="removeWebsiteRow(this)" title="Usuń">&#x2715;</button>';
list.appendChild(row);
if (idx === 0) row.querySelector('input[type="radio"]').checked = true;