Files
Assetmanager/app/templates/categories.html
T

297 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}