Trainingsplanung und Rahmenplanung #9

Merged
Lars merged 29 commits from develop into main 2026-05-05 16:05:01 +02:00
4 changed files with 106 additions and 22 deletions
Showing only changes of commit 88fc9d9ba5 - Show all commits

View File

@ -2713,12 +2713,12 @@ a.analysis-split__nav-item {
accent-color: var(--accent); accent-color: var(--accent);
} }
/* Rahmenprogramm bearbeiten — Mobile Tabs, Desktop Ziele | Slots nebeneinander */ /* Rahmenprogramm bearbeiten — Mobile Tabs, Desktop Ziele | Slots nebeneinander (synchron zu FRAMEWORK_DESKTOP_MIN_PX im Editor) */
.framework-edit { .framework-edit {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
@media (min-width: 1024px) { @media (min-width: 900px) {
.framework-edit { .framework-edit {
max-width: min(1200px, 100%); max-width: min(1200px, 100%);
} }
@ -2772,12 +2772,42 @@ a.analysis-split__nav-item {
.framework-edit__goals-slots { .framework-edit__goals-slots {
display: block; display: block;
} }
@media (max-width: 1023px) { @media (max-width: 899px) {
.framework-edit .framework-edit__panel:not(.framework-edit__panel--active) { .framework-edit .framework-edit__panel:not(.framework-edit__panel--active) {
display: none !important; display: none !important;
} }
} }
/* Rahmen-Editor: Slots (= SessionSpalten) horizontal, scrollbar */
.framework-slots-board {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 12px;
align-items: stretch;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 2px 12px;
margin: 0 -4px;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.framework-slots-board .framework-slot-card {
flex: 0 0 min(320px, calc(100vw - 48px));
min-width: min(320px, calc(100vw - 48px));
max-height: min(70vh, 720px);
overflow-x: hidden;
overflow-y: auto;
scroll-snap-align: start;
box-sizing: border-box;
}
@media (min-width: 900px) {
.framework-slots-board .framework-slot-card {
flex-basis: 300px;
min-width: 280px;
}
}
@media print { @media print {
.desktop-sidebar, .desktop-sidebar,
.bottom-nav, .bottom-nav,

View File

@ -4,6 +4,9 @@ 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) */
const FRAMEWORK_DESKTOP_MIN_PX = 900
function emptyGoal() { function emptyGoal() {
return { title: '', notes: '' } return { title: '', notes: '' }
} }
@ -178,11 +181,13 @@ export default function TrainingFrameworkProgramEditPage() {
/** Nur schmal: welcher Block sichtbar — Desktop zeigt Stammdaten + zwei Spalten Ziele|Slots */ /** Nur schmal: welcher Block sichtbar — Desktop zeigt Stammdaten + zwei Spalten Ziele|Slots */
const [frameworkTab, setFrameworkTab] = useState('meta') const [frameworkTab, setFrameworkTab] = useState('meta')
const [desktopLayout, setDesktopLayout] = useState( const [desktopLayout, setDesktopLayout] = useState(
typeof window !== 'undefined' ? window.matchMedia('(min-width: 1024px)').matches : false typeof window !== 'undefined'
? window.matchMedia(`(min-width: ${FRAMEWORK_DESKTOP_MIN_PX}px)`).matches
: false
) )
useEffect(() => { useEffect(() => {
const mq = window.matchMedia('(min-width: 1024px)') const mq = window.matchMedia(`(min-width: ${FRAMEWORK_DESKTOP_MIN_PX}px)`)
const apply = () => setDesktopLayout(!!mq.matches) const apply = () => setDesktopLayout(!!mq.matches)
apply() apply()
mq.addEventListener('change', apply) mq.addEventListener('change', apply)
@ -444,6 +449,10 @@ export default function TrainingFrameworkProgramEditPage() {
const panelActive = (key) => desktopLayout || frameworkTab === key const panelActive = (key) => desktopLayout || frameworkTab === key
/** Schmale Ansicht: Sichtbarkeit per Inline (falls globales CSS nicht greift / altes Bundle) */
const panelVisibilityStyle = (key) =>
desktopLayout ? undefined : { display: panelActive(key) ? 'block' : 'none' }
if (loading) { if (loading) {
return ( return (
<div style={{ padding: '2rem', textAlign: 'center' }}> <div style={{ padding: '2rem', textAlign: 'center' }}>
@ -467,16 +476,38 @@ export default function TrainingFrameworkProgramEditPage() {
<div className="card" style={{ marginBottom: '1rem', background: 'var(--surface2)', borderStyle: 'dashed' }}> <div className="card" style={{ marginBottom: '1rem', background: 'var(--surface2)', borderStyle: 'dashed' }}>
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.55, margin: 0 }}> <p style={{ fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.55, margin: 0 }}>
<strong style={{ color: 'var(--text1)' }}>Stand dieser Funktion:</strong> Der Rahmen speichert Ziele, Slots <strong style={{ color: 'var(--text1)' }}>Stand dieser Funktion:</strong> Der Rahmen speichert Ziele, Slots
und Übungslisten noch <strong>ohne</strong> die feinere{' '} und pro Slot eine <strong>Übungsliste</strong> (Stückliste). Eine <strong>volle EinheitenStruktur</strong> wie
<strong>TrainingsplanStruktur pro Einheit</strong> (Abschnitte wie in der Einheitenplanung) und{' '} in der Trainingsplanung (Abschnitte, Notizen, Mikrovorlage pro Slot) ist im Konzept optional (
<strong>ohne Übernahme</strong> in die nächste konkrete Trainingseinheit (geplanter Schritt Warenkorb). <strong>CURR010</strong>: <code>training_plan_template_id</code> pro Slot) in der DB derzeit{' '}
<strong> Ziele</strong> lassen sich noch nicht einzelnen Einheiten oder Übungen zuordnen;{' '} <strong>noch nicht</strong> umgesetzt.{' '}
<strong>Progressionsketten</strong> aus dem ÜbungsGraph sind hier noch nicht eingebunden dafür sind <strong>Übernahme</strong> in konkrete Trainingseinheiten mit Referenz auf den Rahmen (Kopie, editierbar){' '}
API/DatenErweiterungen und ein Folgerelease vorgesehen. ist <strong>Stufe3 / Lineage</strong> vorgesehen. Die SlotSpalten unten sind die geplanten
SessionPositionen <strong>ohne feste Termine</strong> am Rahmen.
</p> </p>
</div> </div>
<div className="framework-edit__tabbar" role="tablist" aria-label="Bereiche"> <div
className="framework-edit__tabbar"
role="tablist"
aria-label="Bereiche"
style={
desktopLayout
? { display: 'none' }
: {
display: 'flex',
gap: 6,
marginBottom: 14,
padding: '6px 0 12px',
borderBottom: '2px solid var(--accent)',
flexWrap: 'nowrap',
overflowX: 'auto',
position: 'sticky',
top: 0,
zIndex: 6,
background: 'var(--bg)',
}
}
>
{[ {[
{ id: 'meta', label: 'Stammdaten' }, { id: 'meta', label: 'Stammdaten' },
{ id: 'goals', label: 'Ziele' }, { id: 'goals', label: 'Ziele' },
@ -500,7 +531,7 @@ export default function TrainingFrameworkProgramEditPage() {
'framework-edit__panel framework-edit__panel--meta card' + 'framework-edit__panel framework-edit__panel--meta card' +
(panelActive('meta') ? ' framework-edit__panel--active' : '') (panelActive('meta') ? ' framework-edit__panel--active' : '')
} }
style={{ marginBottom: '1rem' }} style={{ marginBottom: '1rem', ...(panelVisibilityStyle('meta') || {}) }}
> >
<h3 className="card-title">Stammdaten</h3> <h3 className="card-title">Stammdaten</h3>
<div className="form-row"> <div className="form-row">
@ -607,13 +638,25 @@ export default function TrainingFrameworkProgramEditPage() {
</div> </div>
</div> </div>
<div className="framework-edit__goals-slots"> <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--goals card' +
(panelActive('goals') ? ' framework-edit__panel--active' : '') (panelActive('goals') ? ' framework-edit__panel--active' : '')
} }
style={{ marginBottom: '1rem' }} style={{ marginBottom: '1rem', ...(panelVisibilityStyle('goals') || {}) }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
<h3 className="card-title" style={{ marginBottom: 0 }}> <h3 className="card-title" style={{ marginBottom: 0 }}>
@ -681,7 +724,7 @@ export default function TrainingFrameworkProgramEditPage() {
'framework-edit__panel framework-edit__panel--slots card' + 'framework-edit__panel framework-edit__panel--slots card' +
(panelActive('slots') ? ' framework-edit__panel--active' : '') (panelActive('slots') ? ' framework-edit__panel--active' : '')
} }
style={{ marginBottom: '1.5rem' }} style={{ marginBottom: '1.5rem', ...(panelVisibilityStyle('slots') || {}) }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
<h3 className="card-title" style={{ marginBottom: 0 }}> <h3 className="card-title" style={{ marginBottom: 0 }}>
@ -694,15 +737,21 @@ export default function TrainingFrameworkProgramEditPage() {
{form.slots.length === 0 ? ( {form.slots.length === 0 ? (
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}> <p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
Noch keine Slots mit <strong>+ Slot</strong> legst du z.B. Woche 1 / Einheit A an und ordnest Übungen zu. Noch keine Slots mit <strong>+ Slot</strong> legst du EinheitenSpalten an (z.B. Woche 1, Einheit
A) und ordnest Übungen zu. Spalten kannst du horizontal scrollen.
</p> </p>
) : null} ) : (
<p style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px' }}>
Slots = geplante Einheiten/Sessions im Überblick nach rechts scrollen, wenn es viele sind.
</p>
)}
<div className="framework-slots-board">
{form.slots.map((slot, si) => ( {form.slots.map((slot, si) => (
<div <div
key={si} key={si}
className="card" className="card framework-slot-card"
style={{ marginBottom: '12px', background: 'var(--surface)', borderStyle: 'dashed' }} style={{ marginBottom: 0, background: 'var(--surface)', borderStyle: 'dashed' }}
> >
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '10px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '10px' }}>
<button type="button" className="btn btn-secondary" onClick={() => moveSlot(si, -1)}> <button type="button" className="btn btn-secondary" onClick={() => moveSlot(si, -1)}>
@ -845,6 +894,7 @@ export default function TrainingFrameworkProgramEditPage() {
))} ))}
</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' }}>
<button type="button" className="btn btn-primary" disabled={saving} onClick={handleSave}> <button type="button" className="btn btn-primary" disabled={saving} onClick={handleSave}>

View File

@ -57,7 +57,11 @@ export default function TrainingFrameworkProgramsListPage() {
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsrahmenprogramme</h1> <h1 style={{ marginBottom: '0.35rem' }}>Trainingsrahmenprogramme</h1>
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '36rem' }}> <p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '36rem' }}>
Mehrere Entwicklungsziele und Übungen über SessionSlots verteilen als Vorlage in der Bibliothek oder Mehrere Entwicklungsziele und Übungen über SessionSlots verteilen als Vorlage in der Bibliothek oder
im Kontext einer Gruppe. im Kontext einer Gruppe.{' '}
<span style={{ color: 'var(--text3)', fontSize: '0.88rem' }}>
In der <strong>Bearbeitungsansicht</strong> gibt es auf schmalen Fenstern Registerkarten, auf breiten
Bildschirmen zwei Spalten (Ziele | Slots).
</span>
</p> </p>
</div> </div>
<Link <Link

View File

@ -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.1.0", TrainingFrameworkProgramEditPage: "1.2.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