feat: enhance TrainingFrameworkProgramEditPage with goal management and UI improvements
- Introduced new goal chip components for better visual representation and interaction. - Added hover tooltips for goal chips to display additional information. - Implemented state management for editing goals and goal menu visibility. - Updated styles in app.css to support new goal chip design and improve layout consistency. - Incremented version of TrainingFrameworkProgramEditPage to 1.4.0 to reflect the latest enhancements.
This commit is contained in:
parent
0971f35402
commit
8f32a6df29
|
|
@ -2856,6 +2856,130 @@ a.analysis-split__nav-item {
|
||||||
border-left: 3px solid var(--accent);
|
border-left: 3px solid var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.framework-goal-chips__hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text3);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu-anchor {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip-wrap {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
padding: 6px 10px 6px 12px;
|
||||||
|
border-radius: 999px 0 0 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
max-width: min(240px, 100%);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip--active {
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip__text {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip__kebab {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text3);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
border-radius: 0 999px 999px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-chip__kebab:hover,
|
||||||
|
.framework-goal-chip:hover {
|
||||||
|
filter: brightness(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-goal-editor {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
list-style: none;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: min(300px, calc(100vw - 32px));
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border2);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu--align-end {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu__item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu__item:hover {
|
||||||
|
background: var(--surface2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-popmenu__item--danger {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
.framework-ctrl.framework-ctrl--xs {
|
.framework-ctrl.framework-ctrl--xs {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -3090,16 +3214,45 @@ a.analysis-split__nav-item {
|
||||||
color: var(--text3);
|
color: var(--text3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.framework-ex-row__toolbar {
|
.framework-ex-row__row2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__variant-spacer {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__menu-anchor {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__kebab {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border2);
|
||||||
|
background: var(--surface);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__kebab:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.framework-ex-row__variant-select {
|
.framework-ex-row__variant-select {
|
||||||
flex: 1 1 140px;
|
flex: 1 1 auto;
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,15 @@ function emptySlot() {
|
||||||
return { title: '', notes: '', training_unit_id: '', exercises: [] }
|
return { title: '', notes: '', training_unit_id: '', exercises: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Native-Tooltip für Ziel-Chips (Hover); kurz halten für OS-Tooltip-Limits */
|
||||||
|
function goalHoverText(g) {
|
||||||
|
const t = (g.title || '').trim() || 'Ohne Titel'
|
||||||
|
const n = (g.notes || '').trim()
|
||||||
|
if (!n) return t
|
||||||
|
const combined = `${t} — ${n}`
|
||||||
|
return combined.length > 280 ? `${combined.slice(0, 277)}…` : combined
|
||||||
|
}
|
||||||
|
|
||||||
function defaultForm() {
|
function defaultForm() {
|
||||||
return {
|
return {
|
||||||
title: '',
|
title: '',
|
||||||
|
|
@ -190,6 +199,9 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
const [units, setUnits] = useState([])
|
const [units, setUnits] = useState([])
|
||||||
const [pickerSlotIdx, setPickerSlotIdx] = useState(null)
|
const [pickerSlotIdx, setPickerSlotIdx] = useState(null)
|
||||||
const [peekId, setPeekId] = useState(null)
|
const [peekId, setPeekId] = useState(null)
|
||||||
|
const [editingGoalIdx, setEditingGoalIdx] = useState(null)
|
||||||
|
const [goalMenuGi, setGoalMenuGi] = useState(null)
|
||||||
|
const [exerciseMenu, setExerciseMenu] = useState(null)
|
||||||
/** Nur schmal: Stammdaten | Plan (Ziele übereinander, darunter Slots) — Desktop: alles sichtbar */
|
/** Nur schmal: Stammdaten | Plan (Ziele übereinander, darunter Slots) — Desktop: alles sichtbar */
|
||||||
const [frameworkTab, setFrameworkTab] = useState('meta')
|
const [frameworkTab, setFrameworkTab] = useState('meta')
|
||||||
const [desktopLayout, setDesktopLayout] = useState(
|
const [desktopLayout, setDesktopLayout] = useState(
|
||||||
|
|
@ -206,6 +218,17 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
return () => mq.removeEventListener('change', apply)
|
return () => mq.removeEventListener('change', apply)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onPointerDown = (e) => {
|
||||||
|
const t = e.target
|
||||||
|
if (t.closest?.('.framework-popmenu-anchor')) return
|
||||||
|
setExerciseMenu(null)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}
|
||||||
|
document.addEventListener('pointerdown', onPointerDown, true)
|
||||||
|
return () => document.removeEventListener('pointerdown', onPointerDown, true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const loadMeta = useCallback(async () => {
|
const loadMeta = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [gr, cl] = await Promise.all([
|
const [gr, cl] = await Promise.all([
|
||||||
|
|
@ -295,6 +318,8 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveGoal = (idx, dir) => {
|
const moveGoal = (idx, dir) => {
|
||||||
|
setEditingGoalIdx(null)
|
||||||
|
setGoalMenuGi(null)
|
||||||
setForm((prev) => {
|
setForm((prev) => {
|
||||||
const j = idx + dir
|
const j = idx + dir
|
||||||
if (j < 0 || j >= prev.goals.length) return prev
|
if (j < 0 || j >= prev.goals.length) return prev
|
||||||
|
|
@ -304,12 +329,25 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addGoal = () => setForm((prev) => ({ ...prev, goals: [...prev.goals, emptyGoal()] }))
|
const addGoal = () => {
|
||||||
const removeGoal = (idx) =>
|
let newIdx = 0
|
||||||
|
setForm((prev) => {
|
||||||
|
const goals = [...prev.goals, emptyGoal()]
|
||||||
|
newIdx = goals.length - 1
|
||||||
|
return { ...prev, goals }
|
||||||
|
})
|
||||||
|
setEditingGoalIdx(newIdx)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGoal = (idx) => {
|
||||||
setForm((prev) => {
|
setForm((prev) => {
|
||||||
const g = prev.goals.filter((_, i) => i !== idx)
|
const g = prev.goals.filter((_, i) => i !== idx)
|
||||||
return { ...prev, goals: g.length ? g : [emptyGoal()] }
|
return { ...prev, goals: g.length ? g : [emptyGoal()] }
|
||||||
})
|
})
|
||||||
|
setEditingGoalIdx(null)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}
|
||||||
|
|
||||||
const moveSlot = (idx, dir) => {
|
const moveSlot = (idx, dir) => {
|
||||||
setForm((prev) => {
|
setForm((prev) => {
|
||||||
|
|
@ -487,33 +525,29 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExerciseDragStart = (e, fromS, fromE) => {
|
const onExerciseDragStart = (e, fromS, fromE) => {
|
||||||
if (!desktopLayout) return
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
e.dataTransfer.setData(DND_FW_EX, JSON.stringify({ fromS, fromE }))
|
e.dataTransfer.setData(DND_FW_EX, JSON.stringify({ fromS, fromE }))
|
||||||
|
setExerciseMenu(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExerciseDragOver = (e) => {
|
const onExerciseDragOver = (e) => {
|
||||||
if (!desktopLayout) return
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.dataTransfer.dropEffect = 'move'
|
e.dataTransfer.dropEffect = 'move'
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSlotDragStart = (e, slotIdx) => {
|
const onSlotDragStart = (e, slotIdx) => {
|
||||||
if (!desktopLayout) return
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
e.dataTransfer.setData(DND_FW_SLOT, JSON.stringify({ slotIdx }))
|
e.dataTransfer.setData(DND_FW_SLOT, JSON.stringify({ slotIdx }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSlotColumnDragOver = (e) => {
|
const onSlotColumnDragOver = (e) => {
|
||||||
if (!desktopLayout) return
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.dataTransfer.dropEffect = 'move'
|
e.dataTransfer.dropEffect = 'move'
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSlotColumnDrop = (e, targetSi) => {
|
const onSlotColumnDrop = (e, targetSi) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!desktopLayout) return
|
|
||||||
const slotRaw = e.dataTransfer.getData(DND_FW_SLOT)
|
const slotRaw = e.dataTransfer.getData(DND_FW_SLOT)
|
||||||
if (slotRaw) {
|
if (slotRaw) {
|
||||||
const { slotIdx } = JSON.parse(slotRaw)
|
const { slotIdx } = JSON.parse(slotRaw)
|
||||||
|
|
@ -740,7 +774,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: '0.75rem',
|
marginBottom: '0.6rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
||||||
|
|
@ -750,72 +784,138 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
+ Ziel
|
+ Ziel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(form.goals || []).map((g, gi) => (
|
<p className="framework-goal-chips__hint">
|
||||||
<div
|
Ziele als Tags — Tippen zum Bearbeiten; am Desktop zeigt der <strong>Titel</strong>-Tooltip die Notizen mit. Am
|
||||||
key={gi}
|
Handy: <strong>⋯</strong> oder Bearbeiten.
|
||||||
className="framework-goal-block"
|
</p>
|
||||||
style={{
|
<div className="framework-goal-chips">
|
||||||
border: '1px solid var(--border)',
|
{(form.goals || []).map((g, gi) => (
|
||||||
borderRadius: '8px',
|
<div key={gi} className="framework-popmenu-anchor framework-goal-chip-wrap">
|
||||||
padding: '10px',
|
|
||||||
marginBottom: '10px',
|
|
||||||
background: 'var(--surface2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginBottom: '8px' }}>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className={
|
||||||
onClick={() => moveGoal(gi, -1)}
|
'framework-goal-chip' + (editingGoalIdx === gi ? ' framework-goal-chip--active' : '')
|
||||||
aria-label="Ziel nach oben"
|
}
|
||||||
|
title={goalHoverText(g)}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingGoalIdx(gi)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
↑
|
<span className="framework-goal-chip__text">
|
||||||
|
{(g.title || '').trim() || `Ziel ${gi + 1}`}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="framework-goal-chip__kebab"
|
||||||
onClick={() => moveGoal(gi, 1)}
|
aria-label="Ziel-Menü"
|
||||||
aria-label="Ziel nach unten"
|
aria-expanded={goalMenuGi === gi}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setGoalMenuGi((prev) => (prev === gi ? null : gi))
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
↓
|
⋮
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => removeGoal(gi)}
|
|
||||||
>
|
|
||||||
Entfernen
|
|
||||||
</button>
|
</button>
|
||||||
|
{goalMenuGi === gi ? (
|
||||||
|
<ul className="framework-popmenu" role="menu">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingGoalIdx(gi)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
moveGoal(gi, -1)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nach vorn in der Liste
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
moveGoal(gi, 1)
|
||||||
|
setGoalMenuGi(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nach hinten in der Liste
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item framework-popmenu__item--danger"
|
||||||
|
onClick={() => removeGoal(gi)}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{editingGoalIdx != null && form.goals[editingGoalIdx] != null ? (
|
||||||
|
<div className="framework-goal-editor">
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Titel *</label>
|
<label className="form-label">Titel *</label>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={g.title}
|
value={form.goals[editingGoalIdx].title}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
goals: prev.goals.map((x, i) => (i === gi ? { ...x, title: e.target.value } : x)),
|
goals: prev.goals.map((x, i) =>
|
||||||
|
i === editingGoalIdx ? { ...x, title: e.target.value } : x
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
<div className="form-row" style={{ marginBottom: 10 }}>
|
||||||
<label className="form-label">Notizen</label>
|
<label className="form-label">Notizen</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-input"
|
className="form-input"
|
||||||
rows={2}
|
rows={2}
|
||||||
value={g.notes}
|
value={form.goals[editingGoalIdx].notes}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
goals: prev.goals.map((x, i) => (i === gi ? { ...x, notes: e.target.value } : x)),
|
goals: prev.goals.map((x, i) =>
|
||||||
|
i === editingGoalIdx ? { ...x, notes: e.target.value } : x
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
onClick={() => setEditingGoalIdx(null)}
|
||||||
|
>
|
||||||
|
Fertig
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card framework-plan-slots" style={{ marginBottom: '1.5rem' }}>
|
<div className="card framework-plan-slots" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
|
@ -838,9 +938,8 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
className="framework-slots-hint"
|
className="framework-slots-hint"
|
||||||
style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px', lineHeight: 1.45 }}
|
style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px', lineHeight: 1.45 }}
|
||||||
>
|
>
|
||||||
{desktopLayout
|
Sessions nebeneinander — nach rechts scrollen bzw. wischen. Reihenfolge mit Griff (⋮⋮ / ☰)
|
||||||
? 'Sessions nebeneinander — nach rechts scrollen. Spalten am linken Griff verschieben, Übungen am Zeilen-Griff (Drag & Drop).'
|
per Drag & Drop möglich (Browser-abhängig bei Touch); zusätzlich Pfeile und Menüs nutzbar.
|
||||||
: 'Sessions nebeneinander — nach rechts wischen bzw. scrollen. Reihenfolge: Pfeile bei Slots und Übungen (kein Ziehen).'}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -850,23 +949,21 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
<div
|
<div
|
||||||
key={si}
|
key={si}
|
||||||
className="card framework-slot-card"
|
className="card framework-slot-card"
|
||||||
onDragOver={desktopLayout ? onSlotColumnDragOver : undefined}
|
onDragOver={onSlotColumnDragOver}
|
||||||
onDrop={desktopLayout ? (e) => onSlotColumnDrop(e, si) : undefined}
|
onDrop={(e) => onSlotColumnDrop(e, si)}
|
||||||
>
|
>
|
||||||
<div className="framework-slot-card__head">
|
<div className="framework-slot-card__head">
|
||||||
{desktopLayout ? (
|
<span
|
||||||
<span
|
role="button"
|
||||||
role="button"
|
tabIndex={0}
|
||||||
tabIndex={0}
|
className="framework-slot-card__drag-handle"
|
||||||
className="framework-slot-card__drag-handle"
|
draggable
|
||||||
draggable
|
onDragStart={(e) => onSlotDragStart(e, si)}
|
||||||
onDragStart={(e) => onSlotDragStart(e, si)}
|
aria-label="Slot ziehen: Reihenfolge ändern"
|
||||||
aria-label="Slot ziehen: Reihenfolge ändern"
|
title="Slot ziehen (Reihenfolge)"
|
||||||
title="Slot ziehen (Reihenfolge)"
|
>
|
||||||
>
|
⋮⋮
|
||||||
⋮⋮
|
</span>
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<div className="framework-slot-card__head-main">
|
<div className="framework-slot-card__head-main">
|
||||||
<span className="framework-slot-card__slot-label">Session {si + 1}</span>
|
<span className="framework-slot-card__slot-label">Session {si + 1}</span>
|
||||||
<input
|
<input
|
||||||
|
|
@ -954,42 +1051,35 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
</div>
|
</div>
|
||||||
{(slot.exercises || []).length === 0 ? (
|
{(slot.exercises || []).length === 0 ? (
|
||||||
<p className="framework-slot-card__empty-hint">
|
<p className="framework-slot-card__empty-hint">
|
||||||
Noch keine Übung — <strong>+ Übung</strong>
|
Noch keine Übung — <strong>+ Übung</strong> oder Übung mit dem Griff ☰ hierher ziehen.
|
||||||
{desktopLayout ? ' oder Zeile hierher ziehen.' : ' antippen.'}
|
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{(slot.exercises || []).map((ex, ei) => (
|
{(slot.exercises || []).map((ex, ei) => (
|
||||||
<div
|
<div
|
||||||
key={ei}
|
key={ei}
|
||||||
className="framework-ex-row"
|
className="framework-ex-row"
|
||||||
onDragOver={desktopLayout ? onExerciseDragOver : undefined}
|
onDragOver={onExerciseDragOver}
|
||||||
onDrop={
|
onDrop={(e) => {
|
||||||
desktopLayout
|
e.preventDefault()
|
||||||
? (e) => {
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
const raw = e.dataTransfer.getData(DND_FW_EX)
|
||||||
e.stopPropagation()
|
if (!raw) return
|
||||||
const raw = e.dataTransfer.getData(DND_FW_EX)
|
const { fromS, fromE } = JSON.parse(raw)
|
||||||
if (!raw) return
|
if (fromS === si && fromE === ei) return
|
||||||
const { fromS, fromE } = JSON.parse(raw)
|
insertExerciseBefore(fromS, fromE, si, ei)
|
||||||
if (fromS === si && fromE === ei) return
|
}}
|
||||||
insertExerciseBefore(fromS, fromE, si, ei)
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{desktopLayout ? (
|
<span
|
||||||
<span
|
className="framework-ex-row__grip"
|
||||||
className="framework-ex-row__grip"
|
draggable
|
||||||
draggable
|
onDragStart={(e) => {
|
||||||
onDragStart={(e) => {
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
onExerciseDragStart(e, si, ei)
|
||||||
onExerciseDragStart(e, si, ei)
|
}}
|
||||||
}}
|
aria-hidden
|
||||||
aria-hidden
|
>
|
||||||
>
|
☰
|
||||||
☰
|
</span>
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<div className="framework-ex-row__body">
|
<div className="framework-ex-row__body">
|
||||||
<div className="framework-ex-row__title-line">
|
<div className="framework-ex-row__title-line">
|
||||||
{ex.exercise_id ? (
|
{ex.exercise_id ? (
|
||||||
|
|
@ -1005,50 +1095,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
<span className="framework-ex-row__id">#{ex.exercise_id}</span>
|
<span className="framework-ex-row__id">#{ex.exercise_id}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="framework-ex-row__toolbar">
|
<div className="framework-ex-row__row2">
|
||||||
{!desktopLayout ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => moveExercise(si, ei, -1)}
|
|
||||||
aria-label="Übung nach oben"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => moveExercise(si, ei, 1)}
|
|
||||||
aria-label="Übung nach unten"
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => setPickerSlotIdx({ slotIdx: si, exerciseIdx: ei })}
|
|
||||||
>
|
|
||||||
Wählen
|
|
||||||
</button>
|
|
||||||
{ex.exercise_id ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => setPeekId(Number(ex.exercise_id))}
|
|
||||||
>
|
|
||||||
Vorschau
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
onClick={() => removeExercise(si, ei)}
|
|
||||||
>
|
|
||||||
Entf.
|
|
||||||
</button>
|
|
||||||
{ex.exercise_id && (ex.variants || []).length > 0 ? (
|
{ex.exercise_id && (ex.variants || []).length > 0 ? (
|
||||||
<select
|
<select
|
||||||
className="form-input framework-ex-row__variant-select"
|
className="form-input framework-ex-row__variant-select"
|
||||||
|
|
@ -1062,27 +1109,119 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
) : null}
|
) : (
|
||||||
|
<span className="framework-ex-row__variant-spacer" aria-hidden />
|
||||||
|
)}
|
||||||
|
<div className="framework-popmenu-anchor framework-ex-row__menu-anchor">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="framework-ex-row__kebab"
|
||||||
|
aria-label="Übung-Aktionen"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={
|
||||||
|
exerciseMenu?.slotIdx === si && exerciseMenu?.exIdx === ei ? 'true' : 'false'
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setExerciseMenu((prev) =>
|
||||||
|
prev?.slotIdx === si && prev?.exIdx === ei
|
||||||
|
? null
|
||||||
|
: { slotIdx: si, exIdx: ei }
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⋮
|
||||||
|
</button>
|
||||||
|
{exerciseMenu?.slotIdx === si && exerciseMenu?.exIdx === ei ? (
|
||||||
|
<ul className="framework-popmenu framework-popmenu--align-end" role="menu">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
setPickerSlotIdx({ slotIdx: si, exerciseIdx: ei })
|
||||||
|
setExerciseMenu(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Übung wählen…
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{ex.exercise_id ? (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
setPeekId(Number(ex.exercise_id))
|
||||||
|
setExerciseMenu(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Vorschau
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
moveExercise(si, ei, -1)
|
||||||
|
setExerciseMenu(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nach oben in diesem Slot
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item"
|
||||||
|
onClick={() => {
|
||||||
|
moveExercise(si, ei, 1)
|
||||||
|
setExerciseMenu(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nach unten in diesem Slot
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="framework-popmenu__item framework-popmenu__item--danger"
|
||||||
|
onClick={() => {
|
||||||
|
removeExercise(si, ei)
|
||||||
|
setExerciseMenu(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{desktopLayout ? (
|
<div
|
||||||
<div
|
className="framework-slot-card__append-drop"
|
||||||
className="framework-slot-card__append-drop"
|
onDragOver={onExerciseDragOver}
|
||||||
onDragOver={onExerciseDragOver}
|
onDrop={(e) => {
|
||||||
onDrop={(e) => {
|
e.preventDefault()
|
||||||
e.preventDefault()
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
const raw = e.dataTransfer.getData(DND_FW_EX)
|
||||||
const raw = e.dataTransfer.getData(DND_FW_EX)
|
if (!raw) return
|
||||||
if (!raw) return
|
const { fromS, fromE } = JSON.parse(raw)
|
||||||
const { fromS, fromE } = JSON.parse(raw)
|
appendExerciseToSlot(fromS, fromE, si)
|
||||||
appendExerciseToSlot(fromS, fromE, si)
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Ans Ende ziehen
|
||||||
Ans Ende ziehen
|
</div>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const PAGE_VERSIONS = {
|
||||||
SkillsPage: "1.0.0",
|
SkillsPage: "1.0.0",
|
||||||
TrainingPlanningPage: "1.3.1",
|
TrainingPlanningPage: "1.3.1",
|
||||||
TrainingFrameworkProgramsListPage: "1.0.0",
|
TrainingFrameworkProgramsListPage: "1.0.0",
|
||||||
TrainingFrameworkProgramEditPage: "1.3.0",
|
TrainingFrameworkProgramEditPage: "1.4.0",
|
||||||
TrainingUnitRunPage: "1.1.0",
|
TrainingUnitRunPage: "1.1.0",
|
||||||
TrainingCoachPage: "1.0.0",
|
TrainingCoachPage: "1.0.0",
|
||||||
AdminCatalogsPage: "2.2.0", // Updated: Frontend API Calls & Field Names für renamed tables
|
AdminCatalogsPage: "2.2.0", // Updated: Frontend API Calls & Field Names für renamed tables
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user