Add first-day inheritance for occurrence days

This commit is contained in:
gitea_admin committed 2026-09-19 16:50:53 +02:00
1 parent 4eef9d9785
commit 20f130f614
3 files changed
+58 -7

No files matched your search

@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE market_occurrence_day
ADD COLUMN inherit_from_first_day BOOLEAN NOT NULL DEFAULT FALSE;
COMMIT;
+50 -6
View File
@@ -15,6 +15,11 @@ const MARKET_MANAGEMENT_STATUSES = [
'completed', 'completed',
]; ];
const MARKET_OCCURRENCE_DAY_INHERITED_FIELDS = [
'stand_start_time', 'stand_end_time', 'setup_start_time', 'setup_end_time',
'delivery_start_time', 'delivery_end_time', 'observations',
];
function market_management_require_positive_id(mixed $value, string $field): int function market_management_require_positive_id(mixed $value, string $field): int
{ {
if (!is_int($value) && !ctype_digit((string) $value)) { if (!is_int($value) && !ctype_digit((string) $value)) {
@@ -533,6 +538,16 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
); );
$days->execute([':occurrence_id' => $occurrenceId]); $days->execute([':occurrence_id' => $occurrenceId]);
$occurrence['days'] = $days->fetchAll(); $occurrence['days'] = $days->fetchAll();
if (count($occurrence['days']) > 1) {
$firstDay = $occurrence['days'][0];
foreach ($occurrence['days'] as $index => $day) {
if ($index > 0 && (bool) ($day['inherit_from_first_day'] ?? false)) {
foreach (MARKET_OCCURRENCE_DAY_INHERITED_FIELDS as $field) {
$occurrence['days'][$index][$field] = $firstDay[$field] ?? null;
}
}
}
}
$organizer = $pdo->prepare('SELECT * FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id'); $organizer = $pdo->prepare('SELECT * FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id');
$organizer->execute([':occurrence_id' => $occurrenceId]); $organizer->execute([':occurrence_id' => $occurrenceId]);
$organizer = $organizer->fetch(); $organizer = $organizer->fetch();
@@ -690,6 +705,7 @@ function market_management_create_day(PDO $pdo, array $input): array
function market_management_update_day(PDO $pdo, int $dayId, array $input): array function market_management_update_day(PDO $pdo, int $dayId, array $input): array
{ {
$definitions = [ $definitions = [
'inherit_from_first_day' => 'bool',
'date' => 'string', 'stand_start_time' => 'string', 'stand_end_time' => 'string', 'date' => 'string', 'stand_start_time' => 'string', 'stand_end_time' => 'string',
'setup_start_time' => 'string', 'setup_end_time' => 'string', 'delivery_start_time' => 'string', 'setup_start_time' => 'string', 'setup_end_time' => 'string', 'delivery_start_time' => 'string',
'delivery_end_time' => 'string', 'observations' => 'string', 'delivery_end_time' => 'string', 'observations' => 'string',
@@ -698,14 +714,42 @@ function market_management_update_day(PDO $pdo, int $dayId, array $input): array
if ($fields === []) { if ($fields === []) {
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.'); throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
} }
$currentStmt = $pdo->prepare('SELECT occurrence_id, date FROM market_occurrence_day WHERE id = :id');
$currentStmt->execute([':id' => $dayId]);
$currentDay = $currentStmt->fetch();
if (!is_array($currentDay)) {
throw new RuntimeException('Durchführungstag nicht gefunden.');
}
$occurrenceId = (int) $currentDay['occurrence_id'];
if (array_key_exists('date', $input)) { if (array_key_exists('date', $input)) {
$currentStmt = $pdo->prepare('SELECT occurrence_id FROM market_occurrence_day WHERE id = :id'); $params[':date'] = market_management_validate_day_date($pdo, $occurrenceId, (string) $input['date'], $dayId);
$currentStmt->execute([':id' => $dayId]); }
$occurrenceId = $currentStmt->fetchColumn(); if (array_key_exists('inherit_from_first_day', $input) && $params[':inherit_from_first_day']) {
if ($occurrenceId === false) { $firstStmt = $pdo->prepare(
throw new RuntimeException('Durchführungstag nicht gefunden.'); 'SELECT id FROM market_occurrence_day
WHERE occurrence_id = :occurrence_id ORDER BY date ASC, id ASC LIMIT 1'
);
$firstStmt->execute([':occurrence_id' => $occurrenceId]);
if ((int) $firstStmt->fetchColumn() === $dayId) {
throw new InvalidArgumentException('Tag 1 kann nicht von sich selbst erben.');
}
}
if (array_key_exists('inherit_from_first_day', $input) && !$params[':inherit_from_first_day']) {
$sourceStmt = $pdo->prepare(
'SELECT ' . implode(', ', MARKET_OCCURRENCE_DAY_INHERITED_FIELDS) . '
FROM market_occurrence_day
WHERE occurrence_id = :occurrence_id AND id <> :day_id
ORDER BY date ASC, id ASC LIMIT 1'
);
$sourceStmt->execute([':occurrence_id' => (int) $occurrenceId, ':day_id' => $dayId]);
$sourceDay = $sourceStmt->fetch();
if (!is_array($sourceDay)) {
throw new InvalidArgumentException('Für die Übernahme muss Tag 1 vorhanden sein.');
}
foreach (MARKET_OCCURRENCE_DAY_INHERITED_FIELDS as $field) {
$fields[] = $field . ' = :' . $field;
$params[':' . $field] = $sourceDay[$field] ?? null;
} }
$params[':date'] = market_management_validate_day_date($pdo, (int) $occurrenceId, (string) $input['date'], $dayId);
} }
$fields[] = 'updated_at = NOW()'; $fields[] = 'updated_at = NOW()';
$params[':id'] = $dayId; $params[':id'] = $dayId;
+2 -1
View File
@@ -1063,7 +1063,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');"; echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');";
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open); };"; echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };"; echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
echo " const syncMarketDayAddButton = (root = contentRoot) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };"; echo " const syncMarketDayAddButton = (root = contentRoot) => { if (!root.querySelector('[data-market-inherit-row]')) { enhanceMarketDayInheritance(root); } const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };";
echo " const enhanceMarketDayInheritance = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const firstDayId = days[0]?.id; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const dayId = form.dataset.marketEditId; if (!dayId || String(dayId) === String(firstDayId)) { return; } const dateGroup = form.querySelector('[name=\"date\"]')?.closest('.sg-form-sections-card__field-group'); if (!dateGroup) { return; } let row = form.querySelector('[data-market-inherit-row]'); if (!row) { row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketInheritRow = 'true'; row.innerHTML = '<label class=\"sg-checkbox-field-option sg-body\" data-component=\"checkbox-field\" data-component-state=\"default\"><button class=\"sg-checkbox-field\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Ab hier alle Werte aus Tag 1 übernehmen\" data-market-inherit-toggle><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span>Ab hier alle Werte aus Tag 1 übernehmen</span><input type=\"hidden\" name=\"inherit_from_first_day\" value=\"0\"></label>'; dateGroup.after(row); row.querySelector('[data-market-inherit-toggle]').addEventListener('click', () => { const toggle = row.querySelector('[data-market-inherit-toggle]'); const checked = toggle.getAttribute('aria-checked') !== 'true'; toggle.setAttribute('aria-checked', String(checked)); toggle.classList.toggle('sg-form-active', checked); row.querySelector('[name=\"inherit_from_first_day\"]').value = checked ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day') { control.disabled = checked; control.setAttribute('aria-disabled', String(checked)); } }); syncMarketDayAddButton(root); }); } const day = days.find((candidate) => String(candidate.id) === String(dayId)); const inherited = Boolean(day?.inherit_from_first_day); const toggle = row.querySelector('[data-market-inherit-toggle]'); toggle.setAttribute('aria-checked', String(inherited)); toggle.classList.toggle('sg-form-active', inherited); row.querySelector('[name=\"inherit_from_first_day\"]').value = inherited ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day') { control.disabled = inherited; control.setAttribute('aria-disabled', String(inherited)); } }); }); };";
echo " const rerenderMarketDetail = (payload) => { const drawer = getMarketDrawer(); const content = drawer?.querySelector('[data-market-drawer-content]'); if (!drawer || !content) { return; } drawer.dataset.marketPayload = JSON.stringify(payload); content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); };"; echo " const rerenderMarketDetail = (payload) => { const drawer = getMarketDrawer(); const content = drawer?.querySelector('[data-market-drawer-content]'); if (!drawer || !content) { return; } drawer.dataset.marketPayload = JSON.stringify(payload); content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); };";
echo " const addMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence) { return; } const days = Array.isArray(payload.occurrence.days) ? payload.occurrence.days : []; if (days.some((day) => day.__draft)) { return; } days.push({ id: 'draft-' + Date.now(), date: '', __draft: true }); payload.occurrence.days = days; rerenderMarketDetail(payload); const draftEdit = contentRoot.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')?.closest('[data-shared-editable-action-card]')?.querySelector('[data-shared-editable-card-edit]'); draftEdit?.click(); window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); };"; echo " const addMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence) { return; } const days = Array.isArray(payload.occurrence.days) ? payload.occurrence.days : []; if (days.some((day) => day.__draft)) { return; } days.push({ id: 'draft-' + Date.now(), date: '', __draft: true }); payload.occurrence.days = days; rerenderMarketDetail(payload); const draftEdit = contentRoot.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')?.closest('[data-shared-editable-action-card]')?.querySelector('[data-shared-editable-card-edit]'); draftEdit?.click(); window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); };";
echo " const discardMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence || !Array.isArray(payload.occurrence.days)) { return; } const days = payload.occurrence.days.filter((day) => !day.__draft); if (days.length === payload.occurrence.days.length) { return; } payload.occurrence.days = days; rerenderMarketDetail(payload); };"; echo " const discardMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence || !Array.isArray(payload.occurrence.days)) { return; } const days = payload.occurrence.days.filter((day) => !day.__draft); if (days.length === payload.occurrence.days.length) { return; } payload.occurrence.days = days; rerenderMarketDetail(payload); };";