feat: add Roadmap admin page with kanban-style board
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

New /admin/roadmap page showing feature requests from members in
three columns: Planned, In Progress, Done. Cards expand on click
to show implementation details. First item: multi-location support
requested by Daniel Kochański (Stalpunkt).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-09 22:29:11 +02:00
parent 776e84b3dc
commit 528bd3d727
5 changed files with 221 additions and 2 deletions

View File

@ -37,3 +37,4 @@ from . import routes_portal_seo # noqa: E402, F401
from . import routes_user_insights # noqa: E402, F401
from . import routes_user_activity # noqa: E402, F401
from . import routes_company_wizard # noqa: E402, F401
from . import routes_roadmap # noqa: E402, F401

View File

@ -0,0 +1,75 @@
"""
Admin Roadmap Routes
====================
Product roadmap feature requests from members and strategic plans.
"""
import logging
from flask import render_template
from flask_login import login_required
from . import bp
from database import SystemRole
from utils.decorators import role_required
logger = logging.getLogger(__name__)
ROADMAP_ITEMS = [
{
'id': 1,
'title': 'Wiele lokalizacji firmy na mapie',
'description': 'Firmy z kilkoma oddziałami/punktami mogą dodać wiele adresów. '
'Każdy wyświetlany na profilu z linkiem do Google Maps.',
'details': [
'Nowa tabela company_locations (label, adres, lat/lng na przyszłość)',
'Edycja w zakładce Kontakt w profilu firmy (wzorzec jak CompanyWebsite)',
'Dynamiczne wiersze: dodaj/usuń lokalizację, max 10',
'Wyświetlanie na profilu firmy z linkami "Pokaż na mapie"',
],
'requested_by': 'Daniel Kochański (Stalpunkt)',
'date': '2026-04-09',
'priority': 'medium',
'status': 'planned',
'category': 'feature',
},
]
STATUS_LABELS = {
'planned': ('Planowane', '#6366f1'),
'in_progress': ('W trakcie', '#f59e0b'),
'done': ('Zrealizowane', '#10b981'),
'on_hold': ('Wstrzymane', '#94a3b8'),
}
PRIORITY_LABELS = {
'high': ('Wysoki', '#ef4444'),
'medium': ('Średni', '#f59e0b'),
'low': ('Niski', '#6b7280'),
}
CATEGORY_LABELS = {
'feature': 'Nowa funkcjonalność',
'improvement': 'Ulepszenie',
'bugfix': 'Naprawa błędu',
}
@bp.route('/roadmap')
@login_required
@role_required(SystemRole.ADMIN)
def admin_roadmap():
"""Product roadmap — feature requests and development plans."""
planned = [i for i in ROADMAP_ITEMS if i['status'] == 'planned']
in_progress = [i for i in ROADMAP_ITEMS if i['status'] == 'in_progress']
done = [i for i in ROADMAP_ITEMS if i['status'] == 'done']
return render_template(
'admin/roadmap.html',
planned=planned,
in_progress=in_progress,
done=done,
status_labels=STATUS_LABELS,
priority_labels=PRIORITY_LABELS,
category_labels=CATEGORY_LABELS,
)

View File

@ -86,9 +86,17 @@ Najwyższy poziom (Enterprise) służy jako **kotwica cenowa** — sprawia że P
| Projekt Kaszubia — dostęp | - | **+** | + |
| Konta pracowników z rolami | - | - | **+** |
### Roadmapa rozwoju (ze slajdu 9, prezentacja Rada 13.02.2026)
### Roadmapa rozwoju
Funkcje planowane do wdrożenia, wspólnie decydowane z Radą NORDA:
#### Zgłoszenia od członków (backlog)
| # | Funkcjonalność | Opis | Zgłosił | Data | Priorytet | Status |
|---|---------------|------|---------|------|-----------|--------|
| 1 | Wiele lokalizacji firmy na mapie | Firmy z kilkoma oddziałami/punktami mogą dodać wiele adresów. Każdy wyświetlany na profilu z linkiem do Google Maps. Nowa tabela `company_locations` (label, adres, lat/lng na przyszłość), edycja w zakładce Kontakt w profilu firmy, wzorzec jak CompanyWebsite (dynamiczne wiersze, delete-and-reinsert). | Daniel Kochański (Stalpunkt) | 2026-04-09 | Średni | Planowane |
#### Funkcje strategiczne (ze slajdu 9, prezentacja Rada 13.02.2026)
Wspólnie decydowane z Radą NORDA:
| # | Funkcjonalność | Opis |
|---|---------------|------|

View File

@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block title %}Roadmapa - Admin - Norda Biznes Partner{% endblock %}
{% block extra_css %}
.roadmap-header { margin-bottom: var(--spacing-xl); }
.roadmap-header h1 { font-size: var(--font-size-2xl); font-weight: 700; color: var(--text-primary); margin-bottom: var(--spacing-xs); }
.roadmap-header p { color: var(--text-secondary); font-size: var(--font-size-sm); }
.roadmap-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-lg); }
@media (max-width: 1024px) { .roadmap-columns { grid-template-columns: 1fr; } }
.roadmap-column { background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 1px solid var(--border); }
.roadmap-column-header { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-lg); padding-bottom: var(--spacing-md); border-bottom: 2px solid var(--border); }
.roadmap-column-header h2 { font-size: var(--font-size-lg); font-weight: 600; }
.roadmap-column-count { background: var(--background); color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: 600; padding: 2px 8px; border-radius: 10px; }
.roadmap-card { background: var(--background); border-radius: var(--radius); padding: var(--spacing-md); margin-bottom: var(--spacing-md); border: 1px solid var(--border); transition: var(--transition); cursor: pointer; }
.roadmap-card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-md); }
.roadmap-card:last-child { margin-bottom: 0; }
.roadmap-card-title { font-weight: 600; font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-sm); }
.roadmap-card-desc { font-size: var(--font-size-sm); color: var(--text-secondary); line-height: 1.5; margin-bottom: var(--spacing-md); }
.roadmap-card-meta { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); align-items: center; font-size: 12px; }
.roadmap-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.3px; }
.roadmap-card-requester { color: var(--text-secondary); font-size: 12px; }
.roadmap-card-date { color: var(--text-secondary); font-size: 12px; }
.roadmap-details { display: none; margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border); }
.roadmap-details.open { display: block; }
.roadmap-details ul { list-style: none; padding: 0; }
.roadmap-details li { font-size: var(--font-size-sm); color: var(--text-secondary); padding: 4px 0 4px 16px; position: relative; }
.roadmap-details li::before { content: "→"; position: absolute; left: 0; color: var(--primary); }
.roadmap-empty { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); font-size: var(--font-size-sm); }
.col-planned .roadmap-column-header { border-bottom-color: #6366f1; }
.col-progress .roadmap-column-header { border-bottom-color: #f59e0b; }
.col-done .roadmap-column-header { border-bottom-color: #10b981; }
{% endblock %}
{% block content %}
<div class="container">
<div class="roadmap-header">
<h1>Roadmapa rozwoju</h1>
<p>Zgłoszenia od członków i planowane funkcjonalności platformy</p>
</div>
<div class="roadmap-columns">
<div class="roadmap-column col-planned">
<div class="roadmap-column-header">
<h2>Planowane</h2>
<span class="roadmap-column-count">{{ planned|length }}</span>
</div>
{% for item in planned %}
<div class="roadmap-card" onclick="this.querySelector('.roadmap-details').classList.toggle('open')">
<div class="roadmap-card-title">{{ item.title }}</div>
<div class="roadmap-card-desc">{{ item.description }}</div>
<div class="roadmap-card-meta">
<span class="roadmap-badge" style="background: {{ priority_labels[item.priority][1] }}20; color: {{ priority_labels[item.priority][1] }};">{{ priority_labels[item.priority][0] }}</span>
<span class="roadmap-badge" style="background: var(--primary-light); background: #e0e7ff; color: #4338ca;">{{ category_labels.get(item.category, item.category) }}</span>
<span class="roadmap-card-date">{{ item.date }}</span>
</div>
<div class="roadmap-card-requester">Zgłosił: {{ item.requested_by }}</div>
{% if item.details %}
<div class="roadmap-details">
<ul>
{% for detail in item.details %}
<li>{{ detail }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% else %}
<div class="roadmap-empty">Brak zaplanowanych zadań</div>
{% endfor %}
</div>
<div class="roadmap-column col-progress">
<div class="roadmap-column-header">
<h2>W trakcie</h2>
<span class="roadmap-column-count">{{ in_progress|length }}</span>
</div>
{% for item in in_progress %}
<div class="roadmap-card" onclick="this.querySelector('.roadmap-details').classList.toggle('open')">
<div class="roadmap-card-title">{{ item.title }}</div>
<div class="roadmap-card-desc">{{ item.description }}</div>
<div class="roadmap-card-meta">
<span class="roadmap-badge" style="background: {{ priority_labels[item.priority][1] }}20; color: {{ priority_labels[item.priority][1] }};">{{ priority_labels[item.priority][0] }}</span>
<span class="roadmap-card-date">{{ item.date }}</span>
</div>
<div class="roadmap-card-requester">Zgłosił: {{ item.requested_by }}</div>
{% if item.details %}
<div class="roadmap-details">
<ul>
{% for detail in item.details %}
<li>{{ detail }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% else %}
<div class="roadmap-empty">Brak zadań w realizacji</div>
{% endfor %}
</div>
<div class="roadmap-column col-done">
<div class="roadmap-column-header">
<h2>Zrealizowane</h2>
<span class="roadmap-column-count">{{ done|length }}</span>
</div>
{% for item in done %}
<div class="roadmap-card" onclick="this.querySelector('.roadmap-details').classList.toggle('open')">
<div class="roadmap-card-title">{{ item.title }}</div>
<div class="roadmap-card-desc">{{ item.description }}</div>
<div class="roadmap-card-meta">
<span class="roadmap-badge" style="background: #d1fae5; color: #065f46;">{{ category_labels.get(item.category, item.category) }}</span>
<span class="roadmap-card-date">{{ item.date }}</span>
</div>
<div class="roadmap-card-requester">Zgłosił: {{ item.requested_by }}</div>
</div>
{% else %}
<div class="roadmap-empty">Brak zrealizowanych zadań</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -487,6 +487,12 @@
</svg>
Aktywność użytkowników
</a>
<a href="{{ url_for('admin.admin_roadmap') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>
Roadmapa
</a>
{% endif %}
</div>
</div>