asdsd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-29 01:48:53 +02:00
parent 488964df3a
commit 00c879d029

View File

@@ -504,6 +504,11 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
$fxCatalogAvailable = $fxCatalogOptions !== [];
$fxPreferred = is_array($current['preferred_currencies'] ?? null) ? $current['preferred_currencies'] : [];
$fxUseSeparateDb = !empty($current['use_separate_db']);
$fxCatalogJson = json_encode(array_map(
static fn (string $name, string $code): array => ['code' => $code, 'name' => $name],
array_values($fxCatalogOptions),
array_keys($fxCatalogOptions)
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<section class="setup-panel">
<div class="setup-panel__head">
@@ -558,24 +563,36 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<div class="setup-grid">
<label class="setup-field muted">
<span>Anzeige-Basiswaehrung</span>
<select name="display_base_currency">
<?php foreach ($fxCatalogOptions as $code => $name): ?>
<option value="<?= e($code) ?>" <?= (string) ($current['display_base_currency'] ?? '') === $code ? 'selected' : '' ?>>
<?= e($code . ' - ' . $name) ?>
</option>
<?php endforeach; ?>
</select>
<input type="hidden" name="display_base_currency" value="<?= e((string) ($current['display_base_currency'] ?? '')) ?>" data-fx-base-hidden>
<div class="fx-setup-picker" data-fx-base-picker data-currencies='<?= e(is_string($fxCatalogJson) ? $fxCatalogJson : '[]') ?>'>
<input
type="text"
value="<?= e((string) (($current['display_base_currency'] ?? '') !== '' && isset($fxCatalogOptions[(string) $current['display_base_currency']]) ? ((string) $current['display_base_currency'] . ' - ' . $fxCatalogOptions[(string) $current['display_base_currency']]) : '')) ?>"
placeholder="Waehrung suchen"
autocomplete="off"
data-fx-base-input
>
<div class="fx-setup-suggestions" data-fx-base-suggestions hidden></div>
</div>
<small class="muted">Suche nach Code oder Bezeichnung und waehle einen Treffer aus.</small>
</label>
<label class="setup-field muted">
<span>Bevorzugte Waehrungen</span>
<select name="preferred_currencies[]" multiple size="<?= e((string) min(12, max(6, count($fxCatalogOptions)))) ?>">
<?php foreach ($fxCatalogOptions as $code => $name): ?>
<option value="<?= e($code) ?>" <?= in_array($code, $fxPreferred, true) ? 'selected' : '' ?>>
<?= e($code . ' - ' . $name) ?>
</option>
<?php endforeach; ?>
</select>
<small class="muted">Mehrfachauswahl mit Strg/Cmd oder Shift.</small>
<div class="fx-setup-picker" data-fx-multi-picker data-currencies='<?= e(is_string($fxCatalogJson) ? $fxCatalogJson : '[]') ?>'>
<div class="fx-setup-tags" data-fx-tags>
<?php foreach ($fxPreferred as $code): ?>
<?php if (!isset($fxCatalogOptions[$code])) { continue; } ?>
<span class="fx-setup-tag" data-code="<?= e($code) ?>">
<?= e($code) ?>
<button type="button" data-remove-code="<?= e($code) ?>" aria-label="Entfernen">x</button>
</span>
<input type="hidden" name="preferred_currencies[]" value="<?= e($code) ?>" data-code-hidden="<?= e($code) ?>">
<?php endforeach; ?>
</div>
<input type="text" value="" placeholder="Waehrung suchen und hinzufuegen" autocomplete="off" data-fx-multi-input>
<div class="fx-setup-suggestions" data-fx-multi-suggestions hidden></div>
</div>
<small class="muted">Tippen, Treffer anklicken und zur Liste hinzufuegen.</small>
</label>
</div>
<?php endif; ?>
@@ -727,7 +744,232 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
toggle.addEventListener('change', sync);
sync();
})();
(() => {
const baseRoot = document.querySelector('[data-fx-base-picker]');
const baseHidden = document.querySelector('[data-fx-base-hidden]');
const baseInput = document.querySelector('[data-fx-base-input]');
const baseSuggestions = document.querySelector('[data-fx-base-suggestions]');
const multiRoot = document.querySelector('[data-fx-multi-picker]');
const multiInput = document.querySelector('[data-fx-multi-input]');
const multiSuggestions = document.querySelector('[data-fx-multi-suggestions]');
const tagsRoot = document.querySelector('[data-fx-tags]');
const parseCurrencies = (root) => {
if (!root) return [];
try {
const parsed = JSON.parse(root.dataset.currencies || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
};
const currencies = parseCurrencies(baseRoot || multiRoot);
const formatLabel = (item) => `${item.code} - ${item.name}`;
const searchCurrencies = (query, excluded = []) => {
const needle = String(query || '').trim().toLowerCase();
const excludedSet = new Set(excluded);
const filtered = currencies.filter((item) => {
if (!item || !item.code || excludedSet.has(item.code)) {
return false;
}
if (needle === '') {
return true;
}
return item.code.toLowerCase().includes(needle) || String(item.name || '').toLowerCase().includes(needle);
});
return filtered.slice(0, 12);
};
const closeSuggestions = (node) => {
if (!node) return;
node.hidden = true;
node.innerHTML = '';
};
const renderSuggestions = (node, items, onSelect) => {
if (!node || items.length === 0) {
closeSuggestions(node);
return;
}
node.innerHTML = '';
items.forEach((item) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'fx-setup-suggestion';
button.textContent = formatLabel(item);
button.addEventListener('click', () => onSelect(item));
node.appendChild(button);
});
node.hidden = false;
};
if (baseRoot && baseHidden && baseInput && baseSuggestions) {
const applyBase = (item) => {
baseHidden.value = item.code;
baseInput.value = formatLabel(item);
closeSuggestions(baseSuggestions);
};
baseInput.addEventListener('input', () => {
baseHidden.value = '';
renderSuggestions(baseSuggestions, searchCurrencies(baseInput.value), applyBase);
});
baseInput.addEventListener('focus', () => {
renderSuggestions(baseSuggestions, searchCurrencies(baseInput.value), applyBase);
});
baseInput.form?.addEventListener('submit', () => {
if (baseHidden.value !== '') {
return;
}
const first = searchCurrencies(baseInput.value)[0] || null;
if (first) {
applyBase(first);
}
});
}
if (multiRoot && multiInput && multiSuggestions && tagsRoot) {
const selectedCodes = () => Array.from(tagsRoot.querySelectorAll('[data-code]')).map((node) => node.getAttribute('data-code') || '');
const addTag = (item) => {
if (!item || selectedCodes().includes(item.code)) {
return;
}
const tag = document.createElement('span');
tag.className = 'fx-setup-tag';
tag.setAttribute('data-code', item.code);
tag.append(document.createTextNode(item.code));
const remove = document.createElement('button');
remove.type = 'button';
remove.setAttribute('data-remove-code', item.code);
remove.setAttribute('aria-label', 'Entfernen');
remove.textContent = 'x';
remove.addEventListener('click', () => {
tag.remove();
hidden.remove();
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
tag.appendChild(remove);
tagsRoot.appendChild(tag);
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'preferred_currencies[]';
hidden.value = item.code;
hidden.setAttribute('data-code-hidden', item.code);
multiRoot.appendChild(hidden);
multiInput.value = '';
renderSuggestions(multiSuggestions, searchCurrencies('', selectedCodes()), addTag);
};
tagsRoot.querySelectorAll('[data-remove-code]').forEach((button) => {
button.addEventListener('click', () => {
const code = button.getAttribute('data-remove-code') || '';
tagsRoot.querySelector(`[data-code="${code}"]`)?.remove();
multiRoot.querySelector(`[data-code-hidden="${code}"]`)?.remove();
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
});
multiInput.addEventListener('input', () => {
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
multiInput.addEventListener('focus', () => {
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
}
document.addEventListener('click', (event) => {
if (baseRoot && !baseRoot.contains(event.target)) {
closeSuggestions(baseSuggestions);
}
if (multiRoot && !multiRoot.contains(event.target)) {
closeSuggestions(multiSuggestions);
}
});
})();
</script>
<style>
.fx-setup-picker {
position: relative;
display: grid;
gap: 10px;
}
.fx-setup-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.fx-setup-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: color-mix(in srgb, var(--brand-accent) 10%, var(--surface-strong));
font-size: 0.88rem;
font-weight: 700;
}
.fx-setup-tag button {
border: 0;
background: transparent;
color: var(--muted);
cursor: pointer;
font: inherit;
line-height: 1;
padding: 0;
}
.fx-setup-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 20;
display: grid;
gap: 6px;
margin-top: 4px;
padding: 8px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface-strong);
box-shadow: 0 16px 35px rgba(1, 22, 32, 0.14);
max-height: 280px;
overflow: auto;
}
.fx-setup-suggestion {
appearance: none;
text-align: left;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--text);
padding: 8px 10px;
cursor: pointer;
font: inherit;
}
.fx-setup-suggestion:hover,
.fx-setup-suggestion:focus-visible {
border-color: color-mix(in srgb, var(--brand-accent) 36%, transparent);
background: color-mix(in srgb, var(--brand-accent) 8%, var(--surface-strong));
outline: none;
}
</style>
<?php else: ?>
<?php if ($generalFields !== []): ?>
<section class="setup-panel">