feat: enhance TrainingFrameworkProgramEditPage with drag-and-drop functionality and layout improvements
- Updated app.css to refine responsive design for mobile and desktop views, ensuring better usability. - Implemented drag-and-drop features for exercises and slots in TrainingFrameworkProgramEditPage, enhancing user interaction. - Adjusted tab management and layout visibility based on screen size, improving overall user experience. - Incremented version of TrainingFrameworkProgramEditPage to 1.3.0 to reflect the latest enhancements.
This commit is contained in:
parent
88fc9d9ba5
commit
44f224e5d1
|
|
@ -2713,10 +2713,12 @@ a.analysis-split__nav-item {
|
||||||
accent-color: var(--accent);
|
accent-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rahmenprogramm bearbeiten — Mobile Tabs, Desktop Ziele | Slots nebeneinander (synchron zu FRAMEWORK_DESKTOP_MIN_PX im Editor) */
|
/* Rahmenprogramm bearbeiten — Mobile: Stammdaten | Plan; Desktop: untereinander Ziele → Slots (synchron zu FRAMEWORK_DESKTOP_MIN_PX) */
|
||||||
.framework-edit {
|
.framework-edit {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
.framework-edit {
|
.framework-edit {
|
||||||
|
|
@ -2725,13 +2727,11 @@ a.analysis-split__nav-item {
|
||||||
.framework-edit__tabbar {
|
.framework-edit__tabbar {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.framework-edit__goals-slots {
|
.framework-edit__plan-stack {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
/* breit: alle Bereiche sichtbar */
|
|
||||||
.framework-edit__panel {
|
.framework-edit__panel {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
@ -2769,8 +2769,10 @@ a.analysis-split__nav-item {
|
||||||
color: var(--accent-dark);
|
color: var(--accent-dark);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.framework-edit__goals-slots {
|
.framework-edit__plan-stack {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
}
|
}
|
||||||
@media (max-width: 899px) {
|
@media (max-width: 899px) {
|
||||||
.framework-edit .framework-edit__panel:not(.framework-edit__panel--active) {
|
.framework-edit .framework-edit__panel:not(.framework-edit__panel--active) {
|
||||||
|
|
@ -2778,33 +2780,266 @@ a.analysis-split__nav-item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rahmen-Editor: Slots (= Session‑Spalten) horizontal, scrollbar */
|
.framework-plan-goals {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ctrl.framework-ctrl--xs {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
min-height: 26px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontaler Überblick: äußerer Scroll‑Container (Zuverlässigkeit in Flex/Grid‑Eltern) */
|
||||||
|
.framework-slots-board-outer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: -4px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
.framework-slots-board {
|
.framework-slots-board {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow-x: auto;
|
width: max-content;
|
||||||
overflow-y: hidden;
|
min-width: 100%;
|
||||||
padding: 4px 2px 12px;
|
padding: 4px 0 2px;
|
||||||
margin: 0 -4px;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
scroll-snap-type: x proximity;
|
scroll-snap-type: x proximity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.framework-slots-board .framework-slot-card {
|
.framework-slots-board .framework-slot-card {
|
||||||
flex: 0 0 min(320px, calc(100vw - 48px));
|
flex: 0 0 min(300px, calc(100vw - 56px));
|
||||||
min-width: min(320px, calc(100vw - 48px));
|
width: min(300px, calc(100vw - 56px));
|
||||||
max-height: min(70vh, 720px);
|
min-width: min(300px, calc(100vw - 56px));
|
||||||
overflow-x: hidden;
|
height: min(520px, 72vh);
|
||||||
overflow-y: auto;
|
max-height: min(520px, 72vh);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
border-style: dashed;
|
||||||
|
overflow: hidden;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__drag-handle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 6px 4px;
|
||||||
|
color: var(--text3);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__head-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__slot-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__title-input {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__slot-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-details {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-details__summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text2);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-details__summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-details .form-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__exercises {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 10px;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__exercises-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__exercises-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__empty-hint {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text2);
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border2);
|
||||||
|
background: var(--surface2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__grip {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-top: 2px;
|
||||||
|
color: var(--text3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__grip:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__title-line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__title {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__title--muted {
|
||||||
|
color: var(--text3);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-ex-row__variant-select {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework-slot-card__append-drop {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
border: 1px dashed var(--border2);
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
.framework-slots-board .framework-slot-card {
|
.framework-slots-board .framework-slot-card {
|
||||||
flex-basis: 300px;
|
flex-basis: 300px;
|
||||||
min-width: 280px;
|
width: 300px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.framework-slot-card__slot-actions {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,21 @@ import api from '../utils/api'
|
||||||
import ExercisePickerModal from '../components/ExercisePickerModal'
|
import ExercisePickerModal from '../components/ExercisePickerModal'
|
||||||
import ExercisePeekModal from '../components/ExercisePeekModal'
|
import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||||
|
|
||||||
/** Unter dieser Breite: Registerkarten; darüber: Ziele | Slots nebeneinander (muss zu app.css passen) */
|
/** Unter dieser Breite: 2 Tabs (Stammdaten | Plan); darüber: alles untereinander */
|
||||||
const FRAMEWORK_DESKTOP_MIN_PX = 900
|
const FRAMEWORK_DESKTOP_MIN_PX = 900
|
||||||
|
|
||||||
|
const DND_FW_EX = 'application/x-shinkan-framework-exercise'
|
||||||
|
const DND_FW_SLOT = 'application/x-shinkan-framework-slot'
|
||||||
|
|
||||||
|
function reorderArray(arr, from, to) {
|
||||||
|
if (from === to || from < 0 || from >= arr.length) return [...arr]
|
||||||
|
const next = [...arr]
|
||||||
|
const [it] = next.splice(from, 1)
|
||||||
|
const t = Math.max(0, Math.min(to, next.length))
|
||||||
|
next.splice(t, 0, it)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
function emptyGoal() {
|
function emptyGoal() {
|
||||||
return { title: '', notes: '' }
|
return { title: '', notes: '' }
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +190,7 @@ 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)
|
||||||
/** Nur schmal: welcher Block sichtbar — Desktop zeigt Stammdaten + zwei Spalten Ziele|Slots */
|
/** 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(
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
|
|
@ -447,7 +459,82 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelActive = (key) => desktopLayout || frameworkTab === key
|
const insertExerciseBefore = (fromS, fromE, toS, toE) => {
|
||||||
|
setForm((prev) => {
|
||||||
|
const slots = prev.slots.map((s) => ({ ...s, exercises: [...(s.exercises || [])] }))
|
||||||
|
if (fromS < 0 || fromS >= slots.length || toS < 0 || toS >= slots.length) return prev
|
||||||
|
const fromList = slots[fromS].exercises
|
||||||
|
if (fromE < 0 || fromE >= fromList.length) return prev
|
||||||
|
const [moved] = fromList.splice(fromE, 1)
|
||||||
|
let insertAt = toE
|
||||||
|
if (fromS === toS && fromE < toE) insertAt -= 1
|
||||||
|
insertAt = Math.max(0, Math.min(insertAt, slots[toS].exercises.length))
|
||||||
|
slots[toS].exercises.splice(insertAt, 0, moved)
|
||||||
|
return { ...prev, slots }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendExerciseToSlot = (fromS, fromE, toS) => {
|
||||||
|
setForm((prev) => {
|
||||||
|
const slots = prev.slots.map((s) => ({ ...s, exercises: [...(s.exercises || [])] }))
|
||||||
|
if (fromS < 0 || fromS >= slots.length || toS < 0 || toS >= slots.length) return prev
|
||||||
|
const fromList = slots[fromS].exercises
|
||||||
|
if (fromE < 0 || fromE >= fromList.length) return prev
|
||||||
|
const [moved] = fromList.splice(fromE, 1)
|
||||||
|
slots[toS].exercises.push(moved)
|
||||||
|
return { ...prev, slots }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onExerciseDragStart = (e, fromS, fromE) => {
|
||||||
|
if (!desktopLayout) return
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
e.dataTransfer.setData(DND_FW_EX, JSON.stringify({ fromS, fromE }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onExerciseDragOver = (e) => {
|
||||||
|
if (!desktopLayout) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSlotDragStart = (e, slotIdx) => {
|
||||||
|
if (!desktopLayout) return
|
||||||
|
e.stopPropagation()
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
e.dataTransfer.setData(DND_FW_SLOT, JSON.stringify({ slotIdx }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSlotColumnDragOver = (e) => {
|
||||||
|
if (!desktopLayout) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSlotColumnDrop = (e, targetSi) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!desktopLayout) return
|
||||||
|
const slotRaw = e.dataTransfer.getData(DND_FW_SLOT)
|
||||||
|
if (slotRaw) {
|
||||||
|
const { slotIdx } = JSON.parse(slotRaw)
|
||||||
|
if (slotIdx !== targetSi) {
|
||||||
|
setForm((prev) => ({ ...prev, slots: reorderArray([...prev.slots], slotIdx, targetSi) }))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const exRaw = e.dataTransfer.getData(DND_FW_EX)
|
||||||
|
if (exRaw) {
|
||||||
|
const { fromS, fromE } = JSON.parse(exRaw)
|
||||||
|
appendExerciseToSlot(fromS, fromE, targetSi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelActive = (key) => {
|
||||||
|
if (desktopLayout) return true
|
||||||
|
if (key === 'meta') return frameworkTab === 'meta'
|
||||||
|
if (key === 'plan') return frameworkTab === 'plan'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/** Schmale Ansicht: Sichtbarkeit per Inline (falls globales CSS nicht greift / altes Bundle) */
|
/** Schmale Ansicht: Sichtbarkeit per Inline (falls globales CSS nicht greift / altes Bundle) */
|
||||||
const panelVisibilityStyle = (key) =>
|
const panelVisibilityStyle = (key) =>
|
||||||
|
|
@ -510,8 +597,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{ id: 'meta', label: 'Stammdaten' },
|
{ id: 'meta', label: 'Stammdaten' },
|
||||||
{ id: 'goals', label: 'Ziele' },
|
{ id: 'plan', label: 'Plan (Ziele & Sessions)' },
|
||||||
{ id: 'slots', label: 'Slots & Übungen' },
|
|
||||||
].map((t) => (
|
].map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
|
|
@ -638,262 +724,370 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className="framework-edit__goals-slots"
|
|
||||||
style={
|
|
||||||
desktopLayout
|
|
||||||
? {
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1fr 1fr',
|
|
||||||
gap: 16,
|
|
||||||
alignItems: 'start',
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'framework-edit__panel framework-edit__panel--goals card' +
|
'framework-edit__panel framework-edit__panel--plan' +
|
||||||
(panelActive('goals') ? ' framework-edit__panel--active' : '')
|
(panelActive('plan') ? ' framework-edit__panel--active' : '')
|
||||||
}
|
}
|
||||||
style={{ marginBottom: '1rem', ...(panelVisibilityStyle('goals') || {}) }}
|
style={{ marginBottom: '1rem', ...(panelVisibilityStyle('plan') || {}) }}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
<div className="framework-edit__plan-stack">
|
||||||
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
<div className="card framework-plan-goals">
|
||||||
Entwicklungsziele
|
<div
|
||||||
</h3>
|
style={{
|
||||||
<button type="button" className="btn btn-secondary" onClick={addGoal}>
|
display: 'flex',
|
||||||
+ Ziel
|
justifyContent: 'space-between',
|
||||||
</button>
|
alignItems: 'center',
|
||||||
</div>
|
marginBottom: '0.75rem',
|
||||||
{(form.goals || []).map((g, gi) => (
|
}}
|
||||||
<div
|
>
|
||||||
key={gi}
|
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
||||||
style={{
|
Entwicklungsziele
|
||||||
border: '1px solid var(--border)',
|
</h3>
|
||||||
borderRadius: '8px',
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={addGoal}>
|
||||||
padding: '12px',
|
+ Ziel
|
||||||
marginBottom: '10px',
|
|
||||||
background: 'var(--surface2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '8px' }}>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveGoal(gi, -1)}>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveGoal(gi, 1)}>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => removeGoal(gi)}>
|
|
||||||
Entfernen
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
{(form.goals || []).map((g, gi) => (
|
||||||
<label className="form-label">Titel *</label>
|
<div
|
||||||
<input
|
key={gi}
|
||||||
className="form-input"
|
className="framework-goal-block"
|
||||||
value={g.title}
|
style={{
|
||||||
onChange={(e) =>
|
border: '1px solid var(--border)',
|
||||||
setForm((prev) => ({
|
borderRadius: '8px',
|
||||||
...prev,
|
padding: '10px',
|
||||||
goals: prev.goals.map((x, i) => (i === gi ? { ...x, title: e.target.value } : x)),
|
marginBottom: '10px',
|
||||||
}))
|
background: 'var(--surface2)',
|
||||||
}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginBottom: '8px' }}>
|
||||||
<div className="form-row">
|
<button
|
||||||
<label className="form-label">Notizen</label>
|
type="button"
|
||||||
<textarea
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
className="form-input"
|
onClick={() => moveGoal(gi, -1)}
|
||||||
rows={2}
|
aria-label="Ziel nach oben"
|
||||||
value={g.notes}
|
>
|
||||||
onChange={(e) =>
|
↑
|
||||||
setForm((prev) => ({
|
</button>
|
||||||
...prev,
|
<button
|
||||||
goals: prev.goals.map((x, i) => (i === gi ? { ...x, notes: e.target.value } : x)),
|
type="button"
|
||||||
}))
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
}
|
onClick={() => moveGoal(gi, 1)}
|
||||||
/>
|
aria-label="Ziel nach unten"
|
||||||
</div>
|
>
|
||||||
</div>
|
↓
|
||||||
))}
|
</button>
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
<div
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
className={
|
onClick={() => removeGoal(gi)}
|
||||||
'framework-edit__panel framework-edit__panel--slots card' +
|
>
|
||||||
(panelActive('slots') ? ' framework-edit__panel--active' : '')
|
Entfernen
|
||||||
}
|
</button>
|
||||||
style={{ marginBottom: '1.5rem', ...(panelVisibilityStyle('slots') || {}) }}
|
</div>
|
||||||
>
|
<div className="form-row">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
<label className="form-label">Titel *</label>
|
||||||
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
<input
|
||||||
Session‑Slots & Übungen
|
className="form-input"
|
||||||
</h3>
|
value={g.title}
|
||||||
<button type="button" className="btn btn-secondary" onClick={addSlot}>
|
onChange={(e) =>
|
||||||
+ Slot
|
setForm((prev) => ({
|
||||||
</button>
|
...prev,
|
||||||
</div>
|
goals: prev.goals.map((x, i) => (i === gi ? { ...x, title: e.target.value } : x)),
|
||||||
|
}))
|
||||||
{form.slots.length === 0 ? (
|
}
|
||||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
|
/>
|
||||||
Noch keine Slots — mit <strong>+ Slot</strong> legst du Einheiten‑Spalten an (z. B. „Woche 1“, „Einheit
|
</div>
|
||||||
A“) und ordnest Übungen zu. Spalten kannst du horizontal scrollen.
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||||
</p>
|
<label className="form-label">Notizen</label>
|
||||||
) : (
|
<textarea
|
||||||
<p style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px' }}>
|
className="form-input"
|
||||||
Slots = geplante Einheiten/Sessions im Überblick — nach rechts scrollen, wenn es viele sind.
|
rows={2}
|
||||||
</p>
|
value={g.notes}
|
||||||
)}
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({
|
||||||
<div className="framework-slots-board">
|
...prev,
|
||||||
{form.slots.map((slot, si) => (
|
goals: prev.goals.map((x, i) => (i === gi ? { ...x, notes: e.target.value } : x)),
|
||||||
<div
|
}))
|
||||||
key={si}
|
}
|
||||||
className="card framework-slot-card"
|
/>
|
||||||
style={{ marginBottom: 0, background: 'var(--surface)', borderStyle: 'dashed' }}
|
</div>
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '10px' }}>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveSlot(si, -1)}>
|
|
||||||
Slot ↑
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveSlot(si, 1)}>
|
|
||||||
Slot ↓
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => removeSlot(si)}>
|
|
||||||
Slot entfernen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Slot‑Titel</label>
|
|
||||||
<input
|
|
||||||
className="form-input"
|
|
||||||
value={slot.title}
|
|
||||||
onChange={(e) => slotField(si, 'title', e.target.value)}
|
|
||||||
placeholder="z. B. Woche 2 — Technik"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Notizen</label>
|
|
||||||
<textarea
|
|
||||||
className="form-input"
|
|
||||||
rows={2}
|
|
||||||
value={slot.notes}
|
|
||||||
onChange={(e) => slotField(si, 'notes', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{form.plan_mode === 'concrete' && (
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Trainingseinheit (optional)</label>
|
|
||||||
<select
|
|
||||||
className="form-input"
|
|
||||||
value={slot.training_unit_id}
|
|
||||||
onChange={(e) => slotField(si, 'training_unit_id', e.target.value)}
|
|
||||||
disabled={!form.group_id}
|
|
||||||
>
|
|
||||||
<option value="">— keine —</option>
|
|
||||||
{units.map((u) => (
|
|
||||||
<option key={u.id} value={String(u.id)}>
|
|
||||||
{u.planned_date}
|
|
||||||
{u.planned_time_start ? ` ${String(u.planned_time_start).slice(0, 5)}` : ''}
|
|
||||||
{u.planned_focus ? ` · ${u.planned_focus}` : ''}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{!form.group_id ? (
|
|
||||||
<p style={{ fontSize: '0.82rem', color: 'var(--text2)', marginTop: '0.35rem' }}>
|
|
||||||
Wähle oben eine Trainingsgruppe, um geplante Einheiten zu laden.
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card framework-plan-slots" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||||
|
<h3 className="card-title" style={{ marginBottom: 0 }}>
|
||||||
|
Session‑Slots & Übungen
|
||||||
|
</h3>
|
||||||
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={addSlot}>
|
||||||
|
+ Slot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{form.slots.length === 0 ? (
|
||||||
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
|
||||||
|
Noch keine Slots — mit <strong>+ Slot</strong> legst du Spalten an (z. B. „Woche 1“) und ordnest Übungen
|
||||||
|
zu. Bei mehreren Spalten erscheint eine horizontale Scroll-Leiste.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className="framework-slots-hint"
|
||||||
|
style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px', lineHeight: 1.45 }}
|
||||||
|
>
|
||||||
|
{desktopLayout
|
||||||
|
? 'Sessions nebeneinander — nach rechts scrollen. Spalten am linken Griff verschieben, Übungen am Zeilen-Griff (Drag & Drop).'
|
||||||
|
: 'Sessions nebeneinander — nach rechts wischen bzw. scrollen. Reihenfolge: Pfeile bei Slots und Übungen (kein Ziehen).'}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginTop: '12px' }}>
|
<div className="framework-slots-board-outer">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
<div className="framework-slots-board">
|
||||||
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>Übungen</span>
|
{form.slots.map((slot, si) => (
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => addExerciseToSlot(si)}>
|
<div
|
||||||
+ Übung
|
key={si}
|
||||||
</button>
|
className="card framework-slot-card"
|
||||||
</div>
|
onDragOver={desktopLayout ? onSlotColumnDragOver : undefined}
|
||||||
{(slot.exercises || []).length === 0 ? (
|
onDrop={desktopLayout ? (e) => onSlotColumnDrop(e, si) : undefined}
|
||||||
<p style={{ fontSize: '0.85rem', color: 'var(--text2)' }}>
|
>
|
||||||
Über <strong>Übung hinzufügen</strong> auswählen.
|
<div className="framework-slot-card__head">
|
||||||
</p>
|
{desktopLayout ? (
|
||||||
) : null}
|
<span
|
||||||
{(slot.exercises || []).map((ex, ei) => (
|
role="button"
|
||||||
<div
|
tabIndex={0}
|
||||||
key={ei}
|
className="framework-slot-card__drag-handle"
|
||||||
style={{
|
draggable
|
||||||
display: 'grid',
|
onDragStart={(e) => onSlotDragStart(e, si)}
|
||||||
gap: '8px',
|
aria-label="Slot ziehen: Reihenfolge ändern"
|
||||||
padding: '10px',
|
title="Slot ziehen (Reihenfolge)"
|
||||||
marginBottom: '8px',
|
>
|
||||||
borderRadius: '8px',
|
⋮⋮
|
||||||
border: '1px solid var(--border)',
|
</span>
|
||||||
background: 'var(--surface2)',
|
) : null}
|
||||||
}}
|
<div className="framework-slot-card__head-main">
|
||||||
>
|
<span className="framework-slot-card__slot-label">Session {si + 1}</span>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
<input
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveExercise(si, ei, -1)}>
|
className="form-input framework-slot-card__title-input"
|
||||||
↑
|
value={slot.title}
|
||||||
</button>
|
onChange={(e) => slotField(si, 'title', e.target.value)}
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => moveExercise(si, ei, 1)}>
|
placeholder={`z. B. Woche ${si + 1}`}
|
||||||
↓
|
/>
|
||||||
</button>
|
</div>
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => removeExercise(si, ei)}>
|
<div className="framework-slot-card__slot-actions">
|
||||||
Entfernen
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
type="button"
|
onClick={() => moveSlot(si, -1)}
|
||||||
className="btn btn-primary"
|
aria-label="Slot nach links / oben"
|
||||||
onClick={() => setPickerSlotIdx({ slotIdx: si, exerciseIdx: ei })}
|
>
|
||||||
>
|
↑
|
||||||
Übung wählen
|
</button>
|
||||||
</button>
|
<button
|
||||||
{ex.exercise_id ? (
|
type="button"
|
||||||
<button
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
type="button"
|
onClick={() => moveSlot(si, 1)}
|
||||||
className="btn btn-secondary"
|
aria-label="Slot nach rechts / unten"
|
||||||
onClick={() => setPeekId(Number(ex.exercise_id))}
|
>
|
||||||
>
|
↓
|
||||||
Vorschau
|
</button>
|
||||||
</button>
|
<button
|
||||||
) : null}
|
type="button"
|
||||||
</div>
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
<div style={{ fontSize: '0.9rem' }}>
|
onClick={() => removeSlot(si)}
|
||||||
{ex.exercise_id ? (
|
>
|
||||||
<>
|
×
|
||||||
<strong>{ex.exercise_title || `Übung #${ex.exercise_id}`}</strong>
|
</button>
|
||||||
<span style={{ color: 'var(--text2)' }}> (ID {ex.exercise_id})</span>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: 'var(--text3)' }}>Keine Übung gewählt</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{ex.exercise_id && (ex.variants || []).length > 0 ? (
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Variante</label>
|
|
||||||
<select
|
|
||||||
className="form-input"
|
|
||||||
value={ex.exercise_variant_id}
|
|
||||||
onChange={(e) => exerciseField(si, ei, 'exercise_variant_id', e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">— Standard / keine —</option>
|
|
||||||
{(ex.variants || []).map((v) => (
|
|
||||||
<option key={v.id} value={String(v.id)}>
|
|
||||||
{v.variant_name || v.name || `Variante ${v.id}`}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
<details className="framework-slot-details">
|
||||||
))}
|
<summary className="framework-slot-details__summary">Notizen & Einheit</summary>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Notizen</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
value={slot.notes}
|
||||||
|
onChange={(e) => slotField(si, 'notes', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{form.plan_mode === 'concrete' && (
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Trainingseinheit (optional)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={slot.training_unit_id}
|
||||||
|
onChange={(e) => slotField(si, 'training_unit_id', e.target.value)}
|
||||||
|
disabled={!form.group_id}
|
||||||
|
>
|
||||||
|
<option value="">— keine —</option>
|
||||||
|
{units.map((u) => (
|
||||||
|
<option key={u.id} value={String(u.id)}>
|
||||||
|
{u.planned_date}
|
||||||
|
{u.planned_time_start ? ` ${String(u.planned_time_start).slice(0, 5)}` : ''}
|
||||||
|
{u.planned_focus ? ` · ${u.planned_focus}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{!form.group_id ? (
|
||||||
|
<p style={{ fontSize: '0.82rem', color: 'var(--text2)', marginTop: '0.35rem' }}>
|
||||||
|
Wähle in den Stammdaten eine Trainingsgruppe, um geplante Einheiten zu laden.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div className="framework-slot-card__exercises">
|
||||||
|
<div className="framework-slot-card__exercises-head">
|
||||||
|
<span className="framework-slot-card__exercises-title">Übungen</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
onClick={() => addExerciseToSlot(si)}
|
||||||
|
>
|
||||||
|
+ Übung
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(slot.exercises || []).length === 0 ? (
|
||||||
|
<p className="framework-slot-card__empty-hint">
|
||||||
|
Noch keine Übung — <strong>+ Übung</strong>
|
||||||
|
{desktopLayout ? ' oder Zeile hierher ziehen.' : ' antippen.'}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{(slot.exercises || []).map((ex, ei) => (
|
||||||
|
<div
|
||||||
|
key={ei}
|
||||||
|
className="framework-ex-row"
|
||||||
|
onDragOver={desktopLayout ? onExerciseDragOver : undefined}
|
||||||
|
onDrop={
|
||||||
|
desktopLayout
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const raw = e.dataTransfer.getData(DND_FW_EX)
|
||||||
|
if (!raw) return
|
||||||
|
const { fromS, fromE } = JSON.parse(raw)
|
||||||
|
if (fromS === si && fromE === ei) return
|
||||||
|
insertExerciseBefore(fromS, fromE, si, ei)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{desktopLayout ? (
|
||||||
|
<span
|
||||||
|
className="framework-ex-row__grip"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onExerciseDragStart(e, si, ei)
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<div className="framework-ex-row__body">
|
||||||
|
<div className="framework-ex-row__title-line">
|
||||||
|
{ex.exercise_id ? (
|
||||||
|
<strong className="framework-ex-row__title">
|
||||||
|
{ex.exercise_title || `Übung #${ex.exercise_id}`}
|
||||||
|
</strong>
|
||||||
|
) : (
|
||||||
|
<span className="framework-ex-row__title framework-ex-row__title--muted">
|
||||||
|
Keine Übung gewählt
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ex.exercise_id ? (
|
||||||
|
<span className="framework-ex-row__id">#{ex.exercise_id}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="framework-ex-row__toolbar">
|
||||||
|
{!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 ? (
|
||||||
|
<select
|
||||||
|
className="form-input framework-ex-row__variant-select"
|
||||||
|
value={ex.exercise_variant_id}
|
||||||
|
onChange={(e) => exerciseField(si, ei, 'exercise_variant_id', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Variante</option>
|
||||||
|
{(ex.variants || []).map((v) => (
|
||||||
|
<option key={v.id} value={String(v.id)}>
|
||||||
|
{v.variant_name || v.name || `V ${v.id}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{desktopLayout ? (
|
||||||
|
<div
|
||||||
|
className="framework-slot-card__append-drop"
|
||||||
|
onDragOver={onExerciseDragOver}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const raw = e.dataTransfer.getData(DND_FW_EX)
|
||||||
|
if (!raw) return
|
||||||
|
const { fromS, fromE } = JSON.parse(raw)
|
||||||
|
appendExerciseToSlot(fromS, fromE, si)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ans Ende ziehen
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center' }}>
|
||||||
|
|
|
||||||
|
|
@ -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.2.0",
|
TrainingFrameworkProgramEditPage: "1.3.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