bmc_hub/app/devportal/frontend/portal.html
Christian 974876ac67 feat: Implement DEV Portal with Kanban board, idea management, and workflow editor
- Added backend routes for DEV Portal dashboard and workflow editor
- Created frontend templates for portal and editor using Jinja2
- Integrated draw.io for workflow diagram editing and saving
- Developed API endpoints for features, ideas, and workflows management
- Established database schema for features, ideas, and workflows
- Documented DEV Portal functionality, API endpoints, and database structure
2025-12-06 21:27:47 +01:00

622 lines
24 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}DEV Portal - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.nav-pills .nav-link {
color: var(--text-secondary);
border-radius: 8px;
padding: 0.75rem 1.5rem;
margin-bottom: 0.5rem;
}
.nav-pills .nav-link.active {
background: var(--accent);
color: white;
}
.feature-card {
border-left: 4px solid;
transition: transform 0.2s, box-shadow 0.2s;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.status-planlagt { border-color: #6c757d; }
.status-i-gang { border-color: #0d6efd; }
.status-færdig { border-color: #198754; }
.status-sat-på-pause { border-color: #ffc107; }
.idea-card {
transition: transform 0.2s;
}
.idea-card:hover {
transform: scale(1.02);
}
.vote-button {
cursor: pointer;
transition: all 0.2s;
}
.vote-button:hover {
transform: scale(1.1);
color: var(--accent) !important;
}
.workflow-thumbnail {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px;
background: #f8f9fa;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1"><i class="bi bi-code-square me-2"></i>DEV Portal</h2>
<p class="text-muted mb-0">Roadmap, idéer og workflow dokumentation</p>
</div>
<div class="d-flex gap-2" id="actionButtons">
<!-- Dynamic buttons based on active tab -->
</div>
</div>
<!-- Stats Cards -->
<div class="row g-4 mb-4" id="statsCards">
<div class="col-md-3">
<div class="card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1">Features</p>
<h3 class="mb-0" id="featuresCount">-</h3>
</div>
<i class="bi bi-flag text-primary" style="font-size: 2rem;"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1">Idéer</p>
<h3 class="mb-0" id="ideasCount">-</h3>
</div>
<i class="bi bi-lightbulb text-warning" style="font-size: 2rem;"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1">Workflows</p>
<h3 class="mb-0" id="workflowsCount">-</h3>
</div>
<i class="bi bi-diagram-3 text-success" style="font-size: 2rem;"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1">I Gang</p>
<h3 class="mb-0" id="inProgressCount">-</h3>
</div>
<i class="bi bi-gear text-info" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<ul class="nav nav-pills mb-4" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#roadmap">
<i class="bi bi-calendar3 me-2"></i>Roadmap
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#ideas">
<i class="bi bi-lightbulb me-2"></i>Idéer & Brainstorm
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#workflows">
<i class="bi bi-diagram-3 me-2"></i>Workflows
</a>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content">
<!-- Roadmap Tab -->
<div class="tab-pane fade show active" id="roadmap">
<!-- Version Filter -->
<div class="mb-4">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary active" onclick="filterByVersion(null)">Alle</button>
<button type="button" class="btn btn-outline-primary" onclick="filterByVersion('V1')">V1</button>
<button type="button" class="btn btn-outline-primary" onclick="filterByVersion('V2')">V2</button>
<button type="button" class="btn btn-outline-primary" onclick="filterByVersion('V3')">V3</button>
</div>
</div>
<!-- Kanban Board -->
<div class="row g-4">
<div class="col-md-3">
<div class="card p-3">
<h6 class="fw-bold mb-3 text-secondary">📋 Planlagt</h6>
<div id="planlagt-features" class="feature-column"></div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<h6 class="fw-bold mb-3 text-primary">⚙️ I Gang</h6>
<div id="i-gang-features" class="feature-column"></div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<h6 class="fw-bold mb-3 text-success">✅ Færdig</h6>
<div id="færdig-features" class="feature-column"></div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<h6 class="fw-bold mb-3 text-warning">⏸️ På Pause</h6>
<div id="sat-på-pause-features" class="feature-column"></div>
</div>
</div>
</div>
</div>
<!-- Ideas Tab -->
<div class="tab-pane fade" id="ideas">
<div class="row g-4" id="ideasGrid">
<!-- Dynamic ideas cards -->
</div>
</div>
<!-- Workflows Tab -->
<div class="tab-pane fade" id="workflows">
<div class="row g-4" id="workflowsGrid">
<!-- Dynamic workflow cards -->
</div>
</div>
</div>
<!-- Create Feature Modal -->
<div class="modal fade" id="createFeatureModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ny Feature</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createFeatureForm">
<div class="mb-3">
<label class="form-label">Titel *</label>
<input type="text" class="form-control" id="featureTitle" required>
</div>
<div class="mb-3">
<label class="form-label">Beskrivelse</label>
<textarea class="form-control" id="featureDescription" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Version</label>
<select class="form-select" id="featureVersion">
<option value="">Vælg version</option>
<option value="V1">V1</option>
<option value="V2">V2</option>
<option value="V3">V3</option>
<option value="V4">V4</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Status</label>
<select class="form-select" id="featureStatus">
<option value="planlagt">Planlagt</option>
<option value="i gang">I Gang</option>
<option value="færdig">Færdig</option>
<option value="sat på pause">På Pause</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Prioritet (0-100)</label>
<input type="number" class="form-control" id="featurePriority" value="50" min="0" max="100">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Forventet Dato</label>
<input type="date" class="form-control" id="featureDate">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="createFeature()">Opret Feature</button>
</div>
</div>
</div>
</div>
<!-- Create Idea Modal -->
<div class="modal fade" id="createIdeaModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ny Idé</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createIdeaForm">
<div class="mb-3">
<label class="form-label">Titel *</label>
<input type="text" class="form-control" id="ideaTitle" required>
</div>
<div class="mb-3">
<label class="form-label">Beskrivelse</label>
<textarea class="form-control" id="ideaDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Kategori</label>
<select class="form-select" id="ideaCategory">
<option value="feature">Feature</option>
<option value="improvement">Forbedring</option>
<option value="bugfix">Bugfix</option>
<option value="research">Research</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="createIdea()">Opret Idé</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let allFeatures = [];
let currentVersionFilter = null;
// Helper functions to open modals
function openCreateFeatureModal() {
const modal = new bootstrap.Modal(document.getElementById('createFeatureModal'));
modal.show();
}
function openCreateIdeaModal() {
const modal = new bootstrap.Modal(document.getElementById('createIdeaModal'));
modal.show();
}
async function loadStats() {
try {
const response = await fetch('/api/v1/devportal/stats');
if (!response.ok) throw new Error('Kunne ikke hente statistik');
const data = await response.json();
document.getElementById('featuresCount').textContent = data.features_count;
document.getElementById('ideasCount').textContent = data.ideas_count;
document.getElementById('workflowsCount').textContent = data.workflows_count;
const inProgress = data.features_by_status.find(s => s.status === 'i gang');
document.getElementById('inProgressCount').textContent = inProgress ? inProgress.count : 0;
} catch (error) {
console.error('Error loading stats:', error);
}
}
async function loadFeatures() {
try {
const response = await fetch('/api/v1/devportal/features');
if (!response.ok) throw new Error('Kunne ikke hente features');
allFeatures = await response.json();
console.log(`📊 Loaded ${allFeatures.length} features`);
displayFeatures();
} catch (error) {
console.error('Error loading features:', error);
alert('Fejl ved indlæsning af features: ' + error.message);
}
}
function displayFeatures() {
const features = currentVersionFilter
? allFeatures.filter(f => f.version === currentVersionFilter)
: allFeatures;
console.log(`🎯 Displaying ${features.length} features (filter: ${currentVersionFilter || 'none'})`);
// Clear columns
['planlagt', 'i gang', 'færdig', 'sat på pause'].forEach(status => {
const column = document.getElementById(`${status}-features`);
if (column) column.innerHTML = '';
});
features.forEach(feature => {
const column = document.getElementById(`${feature.status}-features`);
if (!column) return;
const card = document.createElement('div');
card.className = `card feature-card status-${feature.status} p-3 mb-2`;
card.innerHTML = `
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="fw-bold mb-0">${feature.title}</h6>
<div>
${feature.version ? `<span class="badge bg-secondary me-1">${feature.version}</span>` : ''}
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteFeature(${feature.id})" title="Slet">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
${feature.description ? `<p class="small text-muted mb-2">${feature.description}</p>` : ''}
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Prioritet: ${feature.priority}</small>
${feature.expected_date ? `<small class="text-muted">${new Date(feature.expected_date).toLocaleDateString('da-DK')}</small>` : ''}
</div>
`;
column.appendChild(card);
});
}
function filterByVersion(version) {
currentVersionFilter = version;
// Update active button
document.querySelectorAll('.btn-group .btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
displayFeatures();
}
async function loadIdeas() {
try {
const response = await fetch('/api/v1/devportal/ideas');
if (!response.ok) throw new Error('Kunne ikke hente idéer');
const ideas = await response.json();
const grid = document.getElementById('ideasGrid');
grid.innerHTML = ideas.map(idea => `
<div class="col-md-4">
<div class="card idea-card p-3 h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="fw-bold">${idea.title}</h6>
<span class="badge bg-primary">${idea.category || 'general'}</span>
</div>
${idea.description ? `<p class="text-muted small mb-3">${idea.description}</p>` : ''}
<div class="mt-auto d-flex justify-content-between align-items-center">
<div class="vote-button" onclick="voteIdea(${idea.id})">
<i class="bi bi-hand-thumbs-up me-1"></i>
<span>${idea.votes}</span>
</div>
<button class="btn btn-sm btn-outline-danger" onclick="deleteIdea(${idea.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading ideas:', error);
alert('Fejl ved indlæsning af idéer: ' + error.message);
}
}
async function loadWorkflows() {
try {
const response = await fetch('/api/v1/devportal/workflows');
if (!response.ok) throw new Error('Kunne ikke hente workflows');
const workflows = await response.json();
const grid = document.getElementById('workflowsGrid');
grid.innerHTML = workflows.map(wf => `
<div class="col-md-4">
<div class="card p-3">
<div class="workflow-thumbnail mb-3 d-flex align-items-center justify-content-center">
<i class="bi bi-diagram-3" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
<h6 class="fw-bold">${wf.title}</h6>
${wf.description ? `<p class="text-muted small mb-3">${wf.description}</p>` : ''}
<div class="d-flex gap-2">
<a href="/devportal/editor?id=${wf.id}" class="btn btn-sm btn-primary flex-grow-1">
<i class="bi bi-pencil me-1"></i>Rediger
</a>
<button class="btn btn-sm btn-outline-danger" onclick="deleteWorkflow(${wf.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading workflows:', error);
alert('Fejl ved indlæsning af workflows: ' + error.message);
}
}
async function createFeature() {
const feature = {
title: document.getElementById('featureTitle').value,
description: document.getElementById('featureDescription').value,
version: document.getElementById('featureVersion').value,
status: document.getElementById('featureStatus').value,
priority: parseInt(document.getElementById('featurePriority').value),
expected_date: document.getElementById('featureDate').value || null
};
if (!feature.title) {
alert('Titel er påkrævet');
return;
}
try {
const response = await fetch('/api/v1/devportal/features', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feature)
});
if (!response.ok) {
throw new Error('Kunne ikke oprette feature');
}
const modal = bootstrap.Modal.getInstance(document.getElementById('createFeatureModal'));
if (modal) modal.hide();
document.getElementById('createFeatureForm').reset();
await loadFeatures();
await loadStats();
console.log('✅ Feature created successfully');
} catch (error) {
console.error('Error creating feature:', error);
alert('Fejl ved oprettelse af feature: ' + error.message);
}
}
async function createIdea() {
const idea = {
title: document.getElementById('ideaTitle').value,
description: document.getElementById('ideaDescription').value,
category: document.getElementById('ideaCategory').value
};
if (!idea.title) {
alert('Titel er påkrævet');
return;
}
try {
const response = await fetch('/api/v1/devportal/ideas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(idea)
});
if (!response.ok) {
throw new Error('Kunne ikke oprette idé');
}
const modal = bootstrap.Modal.getInstance(document.getElementById('createIdeaModal'));
if (modal) modal.hide();
document.getElementById('createIdeaForm').reset();
await loadIdeas();
await loadStats();
console.log('✅ Idea created successfully');
} catch (error) {
console.error('Error creating idea:', error);
alert('Fejl ved oprettelse af idé: ' + error.message);
}
}
async function voteIdea(id) {
try {
const response = await fetch(`/api/v1/devportal/ideas/${id}/vote`, { method: 'POST' });
if (!response.ok) {
throw new Error('Kunne ikke stemme');
}
await loadIdeas();
} catch (error) {
console.error('Error voting:', error);
alert('Fejl ved stemning: ' + error.message);
}
}
async function deleteIdea(id) {
if (!confirm('Er du sikker på at du vil slette denne idé?')) return;
try {
const response = await fetch(`/api/v1/devportal/ideas/${id}`, { method: 'DELETE' });
if (!response.ok) {
throw new Error('Kunne ikke slette idé');
}
await loadIdeas();
await loadStats();
} catch (error) {
console.error('Error deleting idea:', error);
alert('Fejl ved sletning: ' + error.message);
}
}
async function deleteFeature(id) {
if (!confirm('Er du sikker på at du vil slette denne feature?')) return;
try {
const response = await fetch(`/api/v1/devportal/features/${id}`, { method: 'DELETE' });
if (!response.ok) {
throw new Error('Kunne ikke slette feature');
}
await loadFeatures();
await loadStats();
} catch (error) {
console.error('Error deleting feature:', error);
alert('Fejl ved sletning: ' + error.message);
}
}
async function deleteWorkflow(id) {
if (!confirm('Er du sikker på at du vil slette denne workflow?')) return;
try {
const response = await fetch(`/api/v1/devportal/workflows/${id}`, { method: 'DELETE' });
if (!response.ok) {
throw new Error('Kunne ikke slette workflow');
}
await loadWorkflows();
await loadStats();
} catch (error) {
console.error('Error deleting workflow:', error);
alert('Fejl ved sletning: ' + error.message);
}
}
// Tab change handling
document.querySelectorAll('a[data-bs-toggle="pill"]').forEach(tab => {
tab.addEventListener('shown.bs.tab', (e) => {
const target = e.target.getAttribute('href');
const buttons = document.getElementById('actionButtons');
if (target === '#roadmap') {
buttons.innerHTML = '<button class="btn btn-primary" onclick="openCreateFeatureModal()"><i class="bi bi-plus-lg me-2"></i>Ny Feature</button>';
} else if (target === '#ideas') {
buttons.innerHTML = '<button class="btn btn-primary" onclick="openCreateIdeaModal()"><i class="bi bi-plus-lg me-2"></i>Ny Idé</button>';
} else if (target === '#workflows') {
buttons.innerHTML = '<a href="/devportal/editor" class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Ny Workflow</a>';
}
});
});
// Initial load
document.addEventListener('DOMContentLoaded', () => {
console.log('DEV Portal loaded - functions available:', {
openCreateFeatureModal: typeof openCreateFeatureModal,
openCreateIdeaModal: typeof openCreateIdeaModal,
createFeature: typeof createFeature,
createIdea: typeof createIdea
});
loadStats();
loadFeatures();
loadIdeas();
loadWorkflows();
// Set initial button
document.getElementById('actionButtons').innerHTML = '<button class="btn btn-primary" onclick="openCreateFeatureModal()"><i class="bi bi-plus-lg me-2"></i>Ny Feature</button>';
});
</script>
{% endblock %}