-
Übungen (Activity Set)
+
{routineLabel}
- Wiederkehrende Routinen an diesem Zielzustand — erscheinen in Today, wenn das Gate{' '}
+ Übungen gehören zu diesem Zielzustand — sie erscheinen in Today, wenn das Gate{' '}
aktiv ist.
@@ -115,7 +138,7 @@ export function GateActivitySetSection({
{!isActiveGate && (
Gate-Status ist {gateStatus} — Übungen werden in Today sichtbar, sobald
- das Gate auf aktiv steht (Stammdaten bearbeiten).
+ das Gate auf aktiv steht.
)}
@@ -124,7 +147,7 @@ export function GateActivitySetSection({
Ausführen → Today
{' '}
- — fällige Übungen dieses Gates abhaken.
+ — fällige Übungen dieser Routine abhaken.
)}
@@ -149,16 +172,7 @@ export function GateActivitySetSection({
placeholder="Hinweise zur Ausführung"
/>
-
- Rhythmus (Tage)
- setIntervalDays(e.target.value)}
- required
- />
-
+
Übung speichern
@@ -168,7 +182,7 @@ export function GateActivitySetSection({
{loading ? (
Übungen werden geladen…
) : practices.length === 0 ? (
-
+
) : (
{practices.map((practice) => (
@@ -179,14 +193,40 @@ export function GateActivitySetSection({
{practice.description}
)}
- {practice.interval_days
- ? `Alle ${practice.interval_days} Tag(e)`
- : 'Kein Intervall'}
+ {formatScheduleLabel(practice.schedule || practice)}
{practice.open_cadence_instance?.due_at
? ` · Fällig: ${formatDueAt(practice.open_cadence_instance.due_at)}`
: ''}
{practice.today_completed ? ' · heute erledigt' : ''}
+ {editingRhythmId === practice.id && (
+
+
+
+ saveRhythm(practice)}
+ >
+ Rhythmus speichern
+
+ setEditingRhythmId(null)}
+ >
+ Abbrechen
+
+
+
+ )}
{canManage && (
@@ -213,17 +253,8 @@ export function GateActivitySetSection({
className="btn btn-secondary btn-sm"
disabled={busy}
onClick={() => {
- const next = window.prompt(
- 'Neues Intervall in Tagen:',
- String(practice.interval_days || 1),
- )
- if (next == null) return
- const days = Number.parseInt(next, 10)
- if (!Number.isFinite(days) || days < 1) {
- setError('Intervall muss mindestens 1 Tag sein.')
- return
- }
- runAction(() => updateRecurring(practice.id, { interval_days: days }))
+ setEditingRhythmId(practice.id)
+ setEditSchedule(scheduleEditorFromPractice(practice))
}}
>
Rhythmus
diff --git a/frontend/src/components/ScheduleEditor.jsx b/frontend/src/components/ScheduleEditor.jsx
new file mode 100644
index 0000000..ebdaa90
--- /dev/null
+++ b/frontend/src/components/ScheduleEditor.jsx
@@ -0,0 +1,142 @@
+import { useMemo, useState } from 'react'
+import {
+ INTERVAL_PRESETS,
+ WEEKDAY_OPTIONS,
+ defaultScheduleValue,
+ detectIntervalPreset,
+ scheduleFromApi,
+ toggleWeekdayMask,
+} from '../utils/scheduleUtils.js'
+
+export function ScheduleEditor({
+ value,
+ onChange,
+ disabled = false,
+ compact = false,
+}) {
+ const current = value || defaultScheduleValue()
+ const [preset, setPreset] = useState(() =>
+ current.schedule_kind === 'weekdays'
+ ? 'weekdays'
+ : detectIntervalPreset(current.interval_days),
+ )
+
+ const showCustomInterval = preset === 'custom'
+
+ function setKind(kind) {
+ if (kind === 'weekdays') {
+ onChange({
+ ...current,
+ schedule_kind: 'weekdays',
+ weekday_mask: current.weekday_mask || WEEKDAY_OPTIONS[0].bit,
+ })
+ setPreset('weekdays')
+ return
+ }
+ const presetDef = INTERVAL_PRESETS.find((p) => p.id === kind) || INTERVAL_PRESETS[0]
+ onChange({
+ ...current,
+ schedule_kind: 'interval',
+ interval_days: presetDef.intervalDays || current.interval_days || 1,
+ })
+ setPreset(kind)
+ }
+
+ const weekdaySummary = useMemo(() => {
+ const selected = WEEKDAY_OPTIONS.filter((d) => (current.weekday_mask || 0) & d.bit)
+ return selected.map((d) => d.label).join(', ')
+ }, [current.weekday_mask])
+
+ return (
+
+ Rhythmus
+
+
+ {INTERVAL_PRESETS.filter((p) => p.id !== 'custom').map((p) => (
+ setKind(p.id)}
+ >
+ {p.label}
+
+ ))}
+ setKind('weekdays')}
+ >
+ Wochentage
+
+ setKind('custom')}
+ >
+ Individuell
+
+
+
+ {current.schedule_kind === 'interval' && showCustomInterval && (
+
+ Alle wie viele Tage?
+
+ onChange({ ...current, interval_days: Number.parseInt(e.target.value, 10) || 1 })
+ }
+ />
+
+ )}
+
+ {current.schedule_kind === 'weekdays' && (
+
+
+ An welchen Tagen? {weekdaySummary ? `(${weekdaySummary})` : ''}
+
+
+ {WEEKDAY_OPTIONS.map((day) => {
+ const active = Boolean((current.weekday_mask || 0) & day.bit)
+ return (
+
+
+ onChange({
+ ...current,
+ weekday_mask: toggleWeekdayMask(current.weekday_mask || 0, day.bit),
+ })
+ }
+ />
+ {day.label}
+
+ )
+ })}
+
+
+ )}
+
+
+ Pause bis (optional)
+ onChange({ ...current, pause_until: e.target.value })}
+ />
+
+
+ )
+}
+
+export function scheduleEditorFromPractice(practice) {
+ return scheduleFromApi(practice?.schedule || {
+ schedule_kind: 'interval',
+ interval_days: practice?.interval_days || 1,
+ weekday_mask: 0,
+ pause_until: null,
+ })
+}
diff --git a/frontend/src/pages/ActionDetailPage.jsx b/frontend/src/pages/ActionDetailPage.jsx
index 037938f..1f875ee 100644
--- a/frontend/src/pages/ActionDetailPage.jsx
+++ b/frontend/src/pages/ActionDetailPage.jsx
@@ -12,6 +12,7 @@ import { updateAction, setActionAssignments } from '../api/actions.js'
import { createInitiativeBlocker } from '../api/blockers.js'
import { ActionHubCard } from '../components/ActionHubCard.jsx'
import { ActionDependenciesSection } from '../components/ActionDependenciesSection.jsx'
+import { ActionScheduleSection } from '../components/ActionScheduleSection.jsx'
import { ActionForm } from '../components/ActionForm.jsx'
import { TasksSection } from '../components/TasksSection.jsx'
import { ErrorState } from '../components/ErrorState.jsx'
@@ -39,6 +40,7 @@ function ActionDetailBody({
roadmapItems = [],
allActions = [],
executionGraph = null,
+ onReload,
}) {
if (editing) {
return (
@@ -80,6 +82,11 @@ function ActionDetailBody({
onReloadActors={actorsState.reload}
detailMode
/>
+
@@ -317,6 +325,7 @@ function ActionDetailNested() {
roadmapItems={ops.roadmapItems}
allActions={ops.actions}
executionGraph={executionGraph}
+ onReload={ops.reload}
/>