Initialer Import des AssetManagers

This commit is contained in:
2026-08-02 21:28:50 +00:00
commit ed72ce8821
256 changed files with 26361 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
{% extends 'base.html' %}
{% block content %}
<div class="toolbar">
<h1>{{ t('app.categories') }}</h1>
<div class="toolbar-actions">
<button type="button" class="button button-secondary" id="category-bulk-edit-button"
data-base-label="✏ {{ t('categories.bulk.edit', 'Feld ändern')|e }}" disabled>
✏ {{ t('categories.bulk.edit', 'Feld ändern') }}
</button>
<button type="button" class="button button-secondary" id="category-fields-button"
data-base-label="⚙ {{ t('categories.fields.manage', 'Felder verwalten')|e }}" disabled>
⚙ {{ t('categories.fields.manage', 'Felder verwalten') }}
</button>
<button type="submit" class="button button-secondary" form="category-bulk-form" id="activate-defaults-button"
data-base-label="✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren')|e }}" disabled>
✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren') }}
</button>
<a class="button" href="/categories/new">{{ t('categories.create') }}</a>
</div>
</div>
<form id="category-bulk-form" method="post" action="/categories/activate-default-fields"></form>
<dialog id="category-bulk-edit-dialog">
<form id="category-bulk-edit-form" method="post" action="/categories/bulk-edit" enctype="multipart/form-data">
<div class="dialog-heading">
<h2>{{ t('categories.bulk.dialog_title', 'Markierte Kategorien ändern') }}</h2>
<button type="button" class="icon-button" data-dialog-close aria-label="{{ t('common.cancel') }}">×</button>
</div>
<p id="category-bulk-selected-count" class="muted"></p>
<span id="category-bulk-edit-ids"></span>
<label>{{ t('categories.bulk.field', 'Feld') }}
<select name="field_name" id="category-bulk-field" required>
<option value="description">{{ t('common.description') }}</option>
<option value="name" id="category-name-option">{{ t('common.name') }}</option>
<option value="image">{{ t('categories.bulk.image', 'Kategoriebild') }}</option>
</select>
</label>
<p id="category-name-hint" class="muted">{{ t('categories.bulk.name_hint', 'Der Name kann nur geändert werden, wenn genau eine Kategorie markiert ist.') }}</p>
<label id="category-text-value-wrap">{{ t('categories.bulk.value', 'Neuer Inhalt') }}
<textarea name="field_value" id="category-bulk-value" rows="4"></textarea>
</label>
<label id="category-image-value-wrap" hidden>{{ t('categories.bulk.image', 'Kategoriebild') }}
<input type="file" name="image" accept="image/*">
<span class="muted">{{ t('categories.bulk.image_hint', 'Dasselbe Bild wird allen markierten Kategorien zugewiesen.') }}</span>
</label>
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="clear_value" id="category-bulk-clear"> {{ t('categories.bulk.clear', 'Ausgewähltes Feld leeren') }}</label>
<div class="form-actions">
<button type="button" class="button button-secondary" data-dialog-close>{{ t('common.cancel') }}</button>
<button type="submit">💾 {{ t('common.save') }}</button>
</div>
</form>
</dialog>
<dialog id="category-fields-dialog" class="wide-dialog">
<form id="category-fields-form" method="post" action="/categories/bulk-fields">
<div class="dialog-heading">
<h2>{{ t('categories.fields.manage', 'Felder verwalten') }}</h2>
<button type="button" class="icon-button" data-fields-dialog-close aria-label="{{ t('common.cancel') }}">×</button>
</div>
<p id="category-fields-selected-count" class="muted"></p>
<span id="category-fields-ids"></span>
<div class="dialog-tabs" role="tablist" aria-label="{{ t('categories.fields.manage', 'Felder verwalten') }}">
<button type="button" class="button button-secondary category-fields-tab is-active" data-operation="add">{{ t('categories.fields.add', 'Hinzufügen') }}</button>
<button type="button" class="button button-secondary category-fields-tab" data-operation="remove">{{ t('categories.fields.remove', 'Entfernen') }}</button>
<button type="button" class="button button-secondary category-fields-tab" data-operation="settings">{{ t('categories.fields.settings', 'Einstellungen ändern') }}</button>
<button type="button" class="button button-secondary category-fields-tab" data-operation="copy">{{ t('categories.fields.copy', 'Übernehmen') }}</button>
</div>
<input type="hidden" name="operation" id="category-fields-operation" value="add">
<section class="category-fields-panel" data-panel="add">
<p class="muted">{{ t('categories.fields.add_help', 'Die ausgewählten Felder werden in allen markierten Kategorien aktiviert. Bereits vorhandene Zuordnungen bleiben erhalten.') }}</p>
<div class="field-choice-grid">
{% for definition in definitions %}
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="field_ids" value="{{ definition.id }}"> {{ t(definition.translation_key, definition.label) }}</label>
{% endfor %}
</div>
</section>
<section class="category-fields-panel" data-panel="remove" hidden>
<p class="muted">{{ t('categories.fields.remove_help', 'Die ausgewählten Felder werden in allen markierten Kategorien deaktiviert. Vorhandene Assetdaten bleiben erhalten. Das Namensfeld kann nicht entfernt werden.') }}</p>
<div class="field-choice-grid">
{% for definition in definitions if definition.field_name != 'name' %}
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="field_ids" value="{{ definition.id }}" disabled> {{ t(definition.translation_key, definition.label) }}</label>
{% endfor %}
</div>
</section>
<section class="category-fields-panel" data-panel="settings" hidden>
<p class="muted">{{ t('categories.fields.settings_help', 'Die Einstellungen werden nur für Kategorien geändert, in denen das Feld aktiv ist.') }}</p>
<label>{{ t('categories.bulk.field', 'Feld') }}
<select name="field_definition_id">
<option value="">{{ t('common.select', 'Auswählen') }}</option>
{% for definition in definitions %}<option value="{{ definition.id }}">{{ t(definition.translation_key, definition.label) }}</option>{% endfor %}
</select>
</label>
<div class="settings-bulk-grid">
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="apply_required"> {{ t('categories.fields.change_required', 'Pflichtfeld-Einstellung ändern') }}</label>
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="required_value"> {{ t('categories.required') }}</label>
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="apply_show_in_list"> {{ t('categories.fields.change_list', 'Listenanzeige ändern') }}</label>
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="show_in_list_value"> {{ t('categories.in_list') }}</label>
</div>
</section>
<section class="category-fields-panel" data-panel="copy" hidden>
<p class="muted">{{ t('categories.fields.copy_help', 'Die komplette Feldzuordnung und deren Einstellungen werden aus einer vorhandenen Kategorie auf alle markierten Kategorien übernommen. Nicht in der Vorlage aktive Felder werden in den Zielkategorien deaktiviert; Assetdaten bleiben erhalten.') }}</p>
<label>{{ t('categories.fields.source', 'Vorlagenkategorie') }}
<select name="source_category_id">
<option value="">{{ t('common.select', 'Auswählen') }}</option>
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
</select>
</label>
<div class="notice warning">{{ t('categories.fields.copy_warning', 'Die Feldkonfiguration der markierten Kategorien wird an die Vorlage angeglichen. Kategorienamen, Beschreibungen, Bilder und Assets werden nicht verändert.') }}</div>
</section>
<div class="form-actions">
<button type="button" class="button button-secondary" data-fields-dialog-close>{{ t('common.cancel') }}</button>
<button type="submit">💾 {{ t('common.save') }}</button>
</div>
</form>
</dialog>
<div class="table-scroll management-table-scroll">
<table class="data-table categories-table">
<thead>
<tr>
<th class="selection-column" data-no-sort="1" data-no-filter="1">
<input type="checkbox" id="select-all-categories" aria-label="{{ t('categories.select_all', 'Alle Kategorien markieren') }}">
</th>
<th>{{ t('common.name') }}</th>
<th>{{ t('common.assets') }}</th>
<th>{{ t('categories.fields_total', 'Felder gesamt') }}</th>
<th>{{ t('categories.active_fields') }}</th>
<th>{{ t('categories.list_fields', 'In Liste') }}</th>
<th>{{ t('categories.attributes', 'Aktive Attribute') }}</th>
<th data-no-filter="1" data-no-sort="1">{{ t('common.actions') }}</th>
</tr>
</thead>
<tbody>
{% for c in categories %}
{% set count = asset_counts.get(c.id, 0) %}
{% set active_fields = c.visible_fields|selectattr('active')|list %}
{% set list_fields = active_fields|selectattr('show_in_list')|list %}
<tr>
<td class="selection-column">
<input class="category-select" type="checkbox" form="category-bulk-form" name="category_ids" value="{{ c.id }}"
aria-label="{{ c.name|e }} {{ t('common.select', 'markieren') }}">
</td>
<td>{{ c.name }}</td>
<td>{{ count }}</td>
<td>{{ c.visible_fields|length }}</td>
<td>{{ active_fields|length }}</td>
<td>{{ list_fields|length }}</td>
<td class="category-attributes">
{% for field in active_fields %}
<span class="attribute-chip{% if field.show_in_list %} attribute-chip-list{% endif %}" title="{{ t('categories.shown_in_list', 'In Liste sichtbar') if field.show_in_list else t('categories.active_only', 'Aktiv, aber nicht in Liste sichtbar') }}">{{ field_label(field) }}</span>
{% else %}
<span class="muted">{{ t('categories.no_active_fields', 'Keine aktiven Felder') }}</span>
{% endfor %}
</td>
<td class="category-actions">
<div class="action-buttons">
<a class="button button-secondary" href="/categories/{{ c.id }}/edit" title="{{ t('common.edit') }}">{{ t('common.edit') }}</a>
<button type="submit" class="button button-secondary" form="duplicate-category-{{ c.id }}" title="{{ t('common.duplicate') }}">{{ t('common.duplicate') }}</button>
<button type="submit" class="button button-danger" form="delete-category-{{ c.id }}"
{% if count > 0 %}disabled title="{{ t('categories.in_use', 'Kategorie wird noch verwendet') }}"{% else %}title="{{ t('common.delete') }}"{% endif %}>
{{ t('common.delete') }}
</button>
</div>
<form id="duplicate-category-{{ c.id }}" method="post" action="/categories/{{ c.id }}/duplicate"></form>
<form id="delete-category-{{ c.id }}" method="post" action="/categories/{{ c.id }}/delete"
onsubmit="return confirm('{{ t('categories.confirm_delete', 'Kategorie wirklich löschen? Das ist nur möglich, wenn sie von keinem Asset verwendet wird.')|e }}');"></form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(() => {
const boxes = [...document.querySelectorAll('.category-select')];
const all = document.getElementById('select-all-categories');
const defaultsButton = document.getElementById('activate-defaults-button');
const editButton = document.getElementById('category-bulk-edit-button');
const fieldsButton = document.getElementById('category-fields-button');
const fieldsDialog = document.getElementById('category-fields-dialog');
const fieldsIds = document.getElementById('category-fields-ids');
const fieldsSelectedCount = document.getElementById('category-fields-selected-count');
const fieldsOperation = document.getElementById('category-fields-operation');
const dialog = document.getElementById('category-bulk-edit-dialog');
const editIds = document.getElementById('category-bulk-edit-ids');
const selectedCount = document.getElementById('category-bulk-selected-count');
const fieldSelect = document.getElementById('category-bulk-field');
const nameOption = document.getElementById('category-name-option');
const nameHint = document.getElementById('category-name-hint');
const textWrap = document.getElementById('category-text-value-wrap');
const imageWrap = document.getElementById('category-image-value-wrap');
const clearBox = document.getElementById('category-bulk-clear');
const visibleBoxes = () => boxes.filter(box => !box.closest('tr')?.hidden && getComputedStyle(box.closest('tr')).display !== 'none');
const selectedBoxes = () => boxes.filter(box => box.checked);
function refresh() {
const selected = selectedBoxes();
const count = selected.length;
const visible = visibleBoxes();
const visibleSelected = visible.filter(box => box.checked).length;
const defaultsLabel = defaultsButton.dataset.baseLabel || '✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren')|e }}';
const fieldsLabel = fieldsButton.dataset.baseLabel || '⚙ {{ t('categories.fields.manage', 'Felder verwalten')|e }}';
const editLabel = editButton.dataset.baseLabel || '✏ {{ t('categories.bulk.edit', 'Feld ändern')|e }}';
defaultsButton.disabled = count === 0;
defaultsButton.textContent = count ? `${defaultsLabel} (${count})` : defaultsLabel;
fieldsButton.disabled = count === 0;
fieldsButton.textContent = count ? `${fieldsLabel} (${count})` : fieldsLabel;
editButton.disabled = count === 0;
editButton.textContent = count ? `${editLabel} (${count})` : editLabel;
all.checked = visible.length > 0 && visibleSelected === visible.length;
all.indeterminate = visibleSelected > 0 && visibleSelected < visible.length;
}
function updateFieldUi() {
const isImage = fieldSelect.value === 'image';
imageWrap.hidden = !isImage;
textWrap.hidden = isImage;
clearBox.checked = false;
}
boxes.forEach(box => box.addEventListener('change', refresh));
all?.addEventListener('change', () => {
visibleBoxes().forEach(box => box.checked = all.checked);
refresh();
});
editButton?.addEventListener('click', () => {
const selected = selectedBoxes();
if (!selected.length) return;
editIds.replaceChildren(...selected.map(box => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'category_ids';
input.value = box.value;
return input;
}));
const countTemplate = {{ t('categories.bulk.selected_count', '{count} Kategorien ausgewählt')|tojson }};
selectedCount.textContent = countTemplate.replace('{count}', selected.length);
nameOption.disabled = selected.length !== 1;
nameHint.hidden = selected.length === 1;
if (nameOption.disabled && fieldSelect.value === 'name') fieldSelect.value = 'description';
updateFieldUi();
dialog.showModal();
});
fieldsButton?.addEventListener('click', () => {
const selected = selectedBoxes();
if (!selected.length) return;
fieldsIds.replaceChildren(...selected.map(box => {
const input = document.createElement('input');
input.type = 'hidden'; input.name = 'category_ids'; input.value = box.value;
return input;
}));
const countTemplate = {{ t('categories.bulk.selected_count', '{count} Kategorien ausgewählt')|tojson }};
fieldsSelectedCount.textContent = countTemplate.replace('{count}', selected.length);
fieldsDialog.showModal();
});
function selectFieldsOperation(operation) {
fieldsOperation.value = operation;
fieldsDialog.querySelectorAll('.category-fields-tab').forEach(button => button.classList.toggle('is-active', button.dataset.operation === operation));
fieldsDialog.querySelectorAll('.category-fields-panel').forEach(panel => panel.hidden = panel.dataset.panel !== operation);
// Only controls in the active panel are submitted. This prevents duplicate
// field_ids from the add/remove panels.
fieldsDialog.querySelectorAll('.category-fields-panel').forEach(panel => {
panel.querySelectorAll('input, select, textarea').forEach(control => control.disabled = panel.dataset.panel !== operation);
});
}
fieldsDialog?.querySelectorAll('.category-fields-tab').forEach(button => button.addEventListener('click', () => selectFieldsOperation(button.dataset.operation)));
fieldsDialog?.querySelectorAll('[data-fields-dialog-close]').forEach(button => button.addEventListener('click', () => fieldsDialog.close()));
selectFieldsOperation('add');
fieldSelect?.addEventListener('change', updateFieldUi);
dialog?.querySelectorAll('[data-dialog-close]').forEach(button => button.addEventListener('click', () => dialog.close()));
// Table filters can change row visibility without changing checkboxes. Keep the
// selection state consistent whenever the shared table tools signal activity.
document.addEventListener('input', event => {
if (event.target.closest('.categories-table')) requestAnimationFrame(refresh);
});
document.addEventListener('change', event => {
if (event.target.closest('.categories-table')) requestAnimationFrame(refresh);
});
refresh();
})();
</script>
{% endblock %}