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:
|
for mgr, cid in managers:
|
||||||
company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''})
|
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)
|
# Widget 1: Upcoming events (3 nearest future events)
|
||||||
upcoming_events = db.query(NordaEvent).filter(
|
upcoming_events = db.query(NordaEvent).filter(
|
||||||
NordaEvent.event_date >= date.today()
|
NordaEvent.event_date >= date.today()
|
||||||
@ -715,7 +719,8 @@ def dashboard():
|
|||||||
recent_forum_topics=recent_forum_topics,
|
recent_forum_topics=recent_forum_topics,
|
||||||
recent_classifieds=recent_classifieds,
|
recent_classifieds=recent_classifieds,
|
||||||
new_companies=new_companies,
|
new_companies=new_companies,
|
||||||
company_managers_map=company_managers_map
|
company_managers_map=company_managers_map,
|
||||||
|
onboarding=onboarding
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@ -471,6 +471,134 @@
|
|||||||
white-space: nowrap;
|
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 */
|
/* Forum topic item */
|
||||||
.topic-category-badge {
|
.topic-category-badge {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@ -622,6 +750,70 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 #}
|
{# Membership CTA for users without company #}
|
||||||
{% if not user_companies %}
|
{% if not user_companies %}
|
||||||
{% if has_pending_application %}
|
{% if has_pending_application %}
|
||||||
@ -1078,5 +1270,27 @@ function showEditPermissionModal(companyId) {
|
|||||||
document.getElementById('editPermissionModal').addEventListener('click', function(e) {
|
document.getElementById('editPermissionModal').addEventListener('click', function(e) {
|
||||||
if (e.target === this) this.classList.remove('active');
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% 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