All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m26s
- Refactored `ExerciseProgressionGraphPanel` to support a create dialog for new progression graphs, improving user experience. - Integrated `ProgressionGraphListCard` for better visualization of existing graphs and streamlined management. - Updated `ProgressionGraphEditor` to handle start/target analysis and improved draft hydration with AI suggestions. - Added utility functions for managing structured responses from AI, enhancing the planning process. - Incremented application version to reflect these updates.
127 lines
4.5 KiB
JavaScript
127 lines
4.5 KiB
JavaScript
import React from 'react'
|
|
import { GitBranch, Lock, Users, Globe, Pencil, Trash2 } from 'lucide-react'
|
|
import { graphGoalQueryFromRow, graphSlotCountFromRow } from '../utils/progressionGraphDraft'
|
|
import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels'
|
|
|
|
const VIS_LABELS = { official: 'Global', club: 'Verein', private: 'Privat' }
|
|
|
|
function visibilityLabel(v) {
|
|
return VIS_LABELS[v] || v || '—'
|
|
}
|
|
|
|
function cardClassName(graph, userId) {
|
|
const vis = graph.visibility || 'private'
|
|
const visKey = vis === 'official' || vis === 'club' || vis === 'private' ? vis : 'private'
|
|
const mine = userId != null && Number(graph.created_by) === Number(userId)
|
|
return ['card', 'exercise-card', 'progression-graph-card', `exercise-card--scope-${visKey}`, mine ? 'exercise-card--mine' : '']
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
}
|
|
|
|
function VisIcon({ visibility }) {
|
|
if (visibility === 'official') return <Globe size={14} aria-hidden="true" />
|
|
if (visibility === 'club') return <Users size={14} aria-hidden="true" />
|
|
return <Lock size={14} aria-hidden="true" />
|
|
}
|
|
|
|
export default function ProgressionGraphListCard({
|
|
graph,
|
|
userId = null,
|
|
onOpen,
|
|
onDelete,
|
|
disabled = false,
|
|
}) {
|
|
const goalQuery = graphGoalQueryFromRow(graph)
|
|
const slotCount = graphSlotCountFromRow(graph)
|
|
const edgesCount = Number(graph.edges_count) || 0
|
|
const description = (graph.description || '').trim()
|
|
|
|
return (
|
|
<article className={cardClassName(graph, userId)}>
|
|
<div className="exercise-card__body">
|
|
<div className="exercise-card-title" style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
|
|
<GitBranch size={18} style={{ flexShrink: 0, marginTop: '2px', color: 'var(--accent)' }} aria-hidden="true" />
|
|
<button
|
|
type="button"
|
|
className="exercise-card__body--clickable"
|
|
style={{
|
|
border: 'none',
|
|
background: 'none',
|
|
padding: 0,
|
|
textAlign: 'left',
|
|
font: 'inherit',
|
|
color: 'inherit',
|
|
cursor: disabled ? 'default' : 'pointer',
|
|
width: '100%',
|
|
}}
|
|
disabled={disabled}
|
|
onClick={() => onOpen?.(graph)}
|
|
>
|
|
{graph.name || `Graph #${graph.id}`}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="exercise-card-tags" style={{ marginTop: '8px' }}>
|
|
<span className="exercise-tag" title={EXERCISE_VISIBILITY_FIELD_LABEL}>
|
|
<VisIcon visibility={graph.visibility} />
|
|
{visibilityLabel(graph.visibility)}
|
|
</span>
|
|
{slotCount != null ? (
|
|
<span className="exercise-tag">{slotCount} Stufen</span>
|
|
) : null}
|
|
<span className="exercise-tag">
|
|
{edgesCount === 1 ? '1 Kante' : `${edgesCount} Kanten`}
|
|
</span>
|
|
</div>
|
|
|
|
{goalQuery ? (
|
|
<p className="exercise-card-summary" style={{ marginTop: '10px' }}>
|
|
<strong style={{ fontWeight: 600, color: 'var(--text2)' }}>Ziel: </strong>
|
|
{goalQuery.length > 160 ? `${goalQuery.slice(0, 160)}…` : goalQuery}
|
|
</p>
|
|
) : description ? (
|
|
<p className="exercise-card-summary" style={{ marginTop: '10px' }}>
|
|
{description.length > 160 ? `${description.slice(0, 160)}…` : description}
|
|
</p>
|
|
) : (
|
|
<p className="exercise-card-summary muted" style={{ marginTop: '10px' }}>
|
|
Noch kein Planungsziel hinterlegt — öffnen und Roadmap anlegen.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className="exercise-card-layout"
|
|
style={{
|
|
padding: '10px 14px 14px',
|
|
borderTop: '1px solid var(--border)',
|
|
display: 'flex',
|
|
gap: '8px',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
style={{ fontSize: '12px', padding: '6px 12px' }}
|
|
disabled={disabled}
|
|
onClick={() => onOpen?.(graph)}
|
|
>
|
|
<Pencil size={14} style={{ marginRight: '4px', verticalAlign: '-2px' }} aria-hidden="true" />
|
|
Bearbeiten
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn"
|
|
style={{ fontSize: '12px', padding: '6px 12px' }}
|
|
disabled={disabled}
|
|
onClick={() => onDelete?.(graph)}
|
|
>
|
|
<Trash2 size={14} style={{ marginRight: '4px', verticalAlign: '-2px' }} aria-hidden="true" />
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
</article>
|
|
)
|
|
}
|