feat(dashboard): Add onboarding progress widget
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
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
Visual timeline showing company profile completion status: - 6 steps computed from existing DB data (no new tables) - Color-coded badges: member/office/auto responsibility - Collapsible with localStorage persistence - Green "complete" state when all steps done - Action links for incomplete member-owned steps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
05a09812a5
commit
b76d2752c6
@ -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()
|
||||
|
||||
@ -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 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Onboarding progress widget #}
|
||||
{% if onboarding and not onboarding.all_complete %}
|
||||
<div class="onboarding-widget">
|
||||
<div class="onboarding-header" onclick="toggleOnboarding()">
|
||||
<h3>
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
Postęp profilu firmy
|
||||
<span class="onboarding-pct">{{ onboarding.completed }}/{{ onboarding.total }}</span>
|
||||
</h3>
|
||||
<svg class="onboarding-chevron" id="onboardingChevron" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</div>
|
||||
<div class="onboarding-progress">
|
||||
<div class="onboarding-progress-fill" style="width: {{ onboarding.percentage }}%; background: {% if onboarding.percentage == 100 %}#22c55e{% elif onboarding.percentage >= 50 %}#3b82f6{% else %}#f59e0b{% endif %};"></div>
|
||||
</div>
|
||||
<div class="onboarding-steps" id="onboardingSteps">
|
||||
{% for step in onboarding.steps %}
|
||||
<div class="onboarding-step {{ 'done' if step.completed else '' }}">
|
||||
<div class="step-icon {{ 'done' if step.completed else 'pending' }}">
|
||||
{% if step.completed %}
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
||||
{% else %}
|
||||
{{ loop.index }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-label {{ 'done' if step.completed else '' }}">{{ step.label }}</div>
|
||||
<div class="step-meta">
|
||||
<span class="step-badge {{ step.responsible }}">{{ step.responsible_label }}</span>
|
||||
{% if not step.completed and step.action_url %}
|
||||
<a href="{{ step.action_url }}" class="step-action">Uzupełnij →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif onboarding and onboarding.all_complete %}
|
||||
<div class="onboarding-widget" style="border-left: 4px solid #22c55e;">
|
||||
<div class="onboarding-header" onclick="toggleOnboarding()">
|
||||
<h3 style="color: #15803d;">
|
||||
<svg width="18" height="18" fill="none" stroke="#22c55e" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Profil firmy kompletny!
|
||||
</h3>
|
||||
<svg class="onboarding-chevron collapsed" id="onboardingChevron" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</div>
|
||||
<div class="onboarding-progress">
|
||||
<div class="onboarding-progress-fill" style="width: 100%; background: #22c55e;"></div>
|
||||
</div>
|
||||
<div class="onboarding-steps" id="onboardingSteps" style="display: none;">
|
||||
{% for step in onboarding.steps %}
|
||||
<div class="onboarding-step done">
|
||||
<div class="step-icon done">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-label done">{{ step.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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) {}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
90
utils/onboarding.py
Normal file
90
utils/onboarding.py
Normal file
@ -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,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user