diff --git a/blueprints/public/routes.py b/blueprints/public/routes.py index bebb52d..f4eb83a 100644 --- a/blueprints/public/routes.py +++ b/blueprints/public/routes.py @@ -658,6 +658,10 @@ def dashboard(): for mgr, cid in managers: company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''}) + # Onboarding progress widget + from utils.onboarding import compute_onboarding_steps + onboarding = compute_onboarding_steps(current_user, user_companies, current_app.root_path) + # Widget 1: Upcoming events (3 nearest future events) upcoming_events = db.query(NordaEvent).filter( NordaEvent.event_date >= date.today() @@ -715,7 +719,8 @@ def dashboard(): recent_forum_topics=recent_forum_topics, recent_classifieds=recent_classifieds, new_companies=new_companies, - company_managers_map=company_managers_map + company_managers_map=company_managers_map, + onboarding=onboarding ) finally: db.close() diff --git a/templates/dashboard.html b/templates/dashboard.html index cc00144..93859c6 100755 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -471,6 +471,134 @@ white-space: nowrap; } + /* Onboarding widget */ + .onboarding-widget { + background: var(--surface, #fff); + border-radius: 12px; + padding: 20px 24px; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); + margin-bottom: 24px; + } + .onboarding-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + } + .onboarding-header h3 { + margin: 0; + font-size: 1rem; + display: flex; + align-items: center; + gap: 8px; + } + .onboarding-pct { + font-size: 0.85rem; + color: var(--text-secondary, #666); + font-weight: 400; + } + .onboarding-chevron { + transition: transform 0.2s; + color: var(--text-secondary, #666); + } + .onboarding-chevron.collapsed { + transform: rotate(-90deg); + } + .onboarding-progress { + height: 6px; + background: var(--border, #e5e7eb); + border-radius: 3px; + margin: 12px 0 0; + overflow: hidden; + } + .onboarding-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.4s ease; + } + .onboarding-steps { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 0; + } + .onboarding-step { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px 0; + position: relative; + } + .onboarding-step:not(:last-child)::after { + content: ''; + position: absolute; + left: 11px; + top: 34px; + bottom: -2px; + width: 2px; + background: var(--border, #e5e7eb); + } + .onboarding-step.done:not(:last-child)::after { + background: #22c55e; + } + .step-icon { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 12px; + z-index: 1; + } + .step-icon.done { + background: #22c55e; + color: #fff; + } + .step-icon.pending { + background: var(--border, #e5e7eb); + color: var(--text-secondary, #999); + } + .step-content { + flex: 1; + min-width: 0; + } + .step-label { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-primary, #333); + } + .step-label.done { + color: var(--text-secondary, #666); + } + .step-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 2px; + } + .step-badge { + font-size: 0.7rem; + font-weight: 600; + padding: 1px 8px; + border-radius: 10px; + } + .step-badge.auto { background: #f3f4f6; color: #6b7280; } + .step-badge.member { background: #dbeafe; color: #1e40af; } + .step-badge.office { background: #fef3c7; color: #92400e; } + .step-badge.both { background: #ede9fe; color: #6d28d9; } + .step-action { + font-size: 0.8rem; + color: var(--primary, #0066cc); + text-decoration: none; + font-weight: 500; + } + .step-action:hover { + text-decoration: underline; + } + /* Forum topic item */ .topic-category-badge { padding: 2px 8px; @@ -622,6 +750,70 @@ {% endif %} + {# Onboarding progress widget #} + {% if onboarding and not onboarding.all_complete %} +
+ {% elif onboarding and onboarding.all_complete %} + + {% endif %} + {# Membership CTA for users without company #} {% if not user_companies %} {% if has_pending_application %} @@ -1078,5 +1270,27 @@ function showEditPermissionModal(companyId) { document.getElementById('editPermissionModal').addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); }); + +// Onboarding widget toggle +function toggleOnboarding() { + const steps = document.getElementById('onboardingSteps'); + const chevron = document.getElementById('onboardingChevron'); + if (!steps) return; + const hidden = steps.style.display === 'none'; + steps.style.display = hidden ? '' : 'none'; + chevron.classList.toggle('collapsed', !hidden); + try { localStorage.setItem('onboarding_collapsed', hidden ? '0' : '1'); } catch(e) {} +} +// Restore collapsed state +(function() { + try { + if (localStorage.getItem('onboarding_collapsed') === '1') { + const s = document.getElementById('onboardingSteps'); + const c = document.getElementById('onboardingChevron'); + if (s) { s.style.display = 'none'; } + if (c) { c.classList.add('collapsed'); } + } + } catch(e) {} +})(); {% endblock %} diff --git a/utils/onboarding.py b/utils/onboarding.py new file mode 100644 index 0000000..9211b18 --- /dev/null +++ b/utils/onboarding.py @@ -0,0 +1,90 @@ +"""Onboarding progress helper — computes steps from existing DB data.""" + +import os + + +def compute_onboarding_steps(user, user_companies, app_root): + """Compute onboarding progress for dashboard widget. + + Args: + user: Current User object (Flask-Login) + user_companies: List of UserCompany objects (with .company loaded) + app_root: current_app.root_path for file checks + + Returns: + dict with steps, completed, total, percentage, all_complete + """ + company = user_companies[0].company if user_companies else None + + steps = [ + { + 'key': 'registration', + 'label': 'Rejestracja konta', + 'completed': True, + 'responsible': 'auto', + 'responsible_label': 'Automatyczne', + }, + { + 'key': 'email_verified', + 'label': 'Weryfikacja adresu e-mail', + 'completed': getattr(user, 'is_verified', False), + 'responsible': 'member', + 'responsible_label': 'Ty', + }, + { + 'key': 'company_linked', + 'label': 'Powiązanie z firmą', + 'completed': len(user_companies) > 0, + 'responsible': 'office', + 'responsible_label': 'Sekretariat', + }, + { + 'key': 'basic_data', + 'label': 'Dane podstawowe firmy', + 'completed': bool( + company + and company.name + and getattr(company, 'nip', None) + and getattr(company, 'address_city', None) + ), + 'responsible': 'office', + 'responsible_label': 'Sekretariat', + }, + { + 'key': 'description', + 'label': 'Opis i oferta firmy', + 'completed': bool( + company + and getattr(company, 'description_full', None) + and getattr(company, 'services_offered', None) + ), + 'responsible': 'member', + 'responsible_label': 'Ty', + 'action_url': f'/company/{company.id}/edit' if company else None, + }, + { + 'key': 'logo', + 'label': 'Logo firmy', + 'completed': bool( + company + and getattr(company, 'slug', None) + and os.path.exists( + os.path.join(app_root, 'static', 'img', 'companies', f'{company.slug}.webp') + ) + ), + 'responsible': 'both', + 'responsible_label': 'Ty / Sekretariat', + 'action_url': f'/company/{company.id}/edit' if company else None, + }, + ] + + completed = sum(1 for s in steps if s['completed']) + total = len(steps) + + return { + 'steps': steps, + 'completed': completed, + 'total': total, + 'percentage': round(completed / total * 100) if total else 0, + 'all_complete': completed == total, + }