snapshot: preserve scalable app registry and landing visibility rules

This commit is contained in:
Md Bayazid Bostame
2026-03-26 12:59:45 +01:00
parent 007d4e329a
commit 9437aaa29a
9 changed files with 762 additions and 242 deletions

View File

@@ -1,5 +1,6 @@
{% extends 'workflows/base_shell.html' %}
{% load static i18n %}
{% trans "Ungespeicherte Änderungen" as dirty_state_label %}
{% block title %}{% trans "App Registry" %}{% endblock %}
@@ -21,63 +22,220 @@
</div>
<form method="post" action="{% url 'save_portal_app_registry' %}" class="stack-form">
{% csrf_token %}
<div class="table-wrap app-registry-wrap">
<table class="app-registry-table">
<thead>
<tr>
<th>{% trans "Key" %}</th>
<th>{% trans "Aktiv" %}</th>
<th>{% trans "Bereich" %}</th>
<th>{% trans "Reihenfolge" %}</th>
<th>{% trans "Titel DE" %}</th>
<th>{% trans "Titel EN" %}</th>
<th>{% trans "Aktion DE" %}</th>
<th>{% trans "Aktion EN" %}</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>
<div><strong>{{ row.config.key }}</strong></div>
<div class="mini">{{ row.definition.title }}</div>
</td>
<td class="select-col">
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
</td>
<td>
<select name="section__{{ row.config.key }}">
{% for value, label in section_choices %}
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td>
<input type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
</td>
<td>
<input type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
<textarea name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
</td>
<td>
<input type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
<textarea name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
</td>
<td>
<input type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
</td>
<td>
<input type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
</td>
</tr>
{% endfor %}
</tbody>
</table>
<section class="app-registry-filters">
<div class="field">
<label for="app-registry-search">{% trans "Suche" %}</label>
<input id="app-registry-search" type="text" placeholder="{% trans 'Nach App-Name oder Key filtern' %}" />
</div>
<div class="field">
<label for="app-registry-state">{% trans "Status" %}</label>
<select id="app-registry-state">
<option value="all">{% trans "Alle" %}</option>
<option value="enabled">{% trans "Aktiv" %}</option>
<option value="disabled">{% trans "Deaktiviert" %}</option>
<option value="platform_only">{% trans "Platform only" %}</option>
</select>
</div>
<div class="field">
<label for="app-registry-section">{% trans "Bereich" %}</label>
<select id="app-registry-section">
<option value="all">{% trans "Alle" %}</option>
<option value="apps">{% trans "Apps" %}</option>
<option value="platform_apps">{% trans "Platform Apps" %}</option>
<option value="admin_apps">{% trans "Admin Apps" %}</option>
</select>
</div>
</section>
<div class="app-registry-cards">
{% for row in rows %}
<details class="app-registry-card{% if not row.config.is_enabled %} is-disabled{% endif %}" data-app-card
data-key="{{ row.config.key|lower }}"
data-title="{{ row.definition.title|lower }}"
data-enabled="{% if row.config.is_enabled %}1{% else %}0{% endif %}"
data-platform-only="{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}1{% else %}0{% endif %}"
data-section="{{ row.config.section }}">
<summary class="app-registry-summary">
<div class="app-registry-summary-main">
<div class="app-registry-card-title-row">
<h2>{{ row.definition.title }}</h2>
{% if row.config.is_enabled %}
<span class="badge sent">{% trans "Aktiv" %}</span>
{% else %}
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
{% endif %}
</div>
<div class="mini">{{ row.config.key }}</div>
<p class="app-registry-card-copy">{{ row.definition.description }}</p>
<p class="mini">{% trans "Empfohlener Standardzugriff:" %} {{ row.default_visibility_summary }}</p>
</div>
<div class="app-registry-summary-meta">
<span class="badge scheduled">
{% if row.config.section == 'platform_apps' %}
{% trans "Platform Apps" %}
{% elif row.config.section == 'admin_apps' %}
{% trans "Admin Apps" %}
{% else %}
{% trans "Apps" %}
{% endif %}
</span>
<span class="badge">{% trans "Sortierung" %}: {{ row.config.sort_order }}</span>
{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}
<span class="badge paused">{% trans "Platform only" %}</span>
{% elif row.config.visible_to_super_admin and row.config.visible_to_admin and row.config.visible_to_it_staff and row.config.visible_to_staff %}
<span class="badge sent">{% trans "Alle Firmenrollen" %}</span>
{% else %}
<span class="badge scheduled">
{% if row.config.visible_to_super_admin %}{% trans "Super Admin" %}{% endif %}
{% if row.config.visible_to_admin %}{% if row.config.visible_to_super_admin %} + {% endif %}{% trans "Admin" %}{% endif %}
{% if row.config.visible_to_it_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin %} + {% endif %}{% trans "IT Staff" %}{% endif %}
{% if row.config.visible_to_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin or row.config.visible_to_it_staff %} + {% endif %}{% trans "Staff" %}{% endif %}
</span>
{% endif %}
</div>
</summary>
<div class="app-registry-card-grid">
<section class="app-registry-panel">
<h3>{% trans "Verfügbarkeit" %}</h3>
<div class="check-row app-registry-checks">
<label>
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
<span>{% trans "App aktiviert" %}</span>
</label>
</div>
<p class="hint">{% trans "Deaktivierte Apps erscheinen nicht auf der Landing Page, selbst wenn Rollen sie sehen dürften." %}</p>
</section>
<section class="app-registry-panel">
<h3>{% trans "Sichtbarkeit nach Rolle" %}</h3>
<div class="check-row app-registry-checks">
<label>
<input type="checkbox" name="visible_to_super_admin__{{ row.config.key }}" {% if row.config.visible_to_super_admin %}checked{% endif %} />
<span>{% trans "Super Admin" %}</span>
</label>
<label>
<input type="checkbox" name="visible_to_admin__{{ row.config.key }}" {% if row.config.visible_to_admin %}checked{% endif %} />
<span>{% trans "Admin" %}</span>
</label>
<label>
<input type="checkbox" name="visible_to_it_staff__{{ row.config.key }}" {% if row.config.visible_to_it_staff %}checked{% endif %} />
<span>{% trans "IT Staff" %}</span>
</label>
<label>
<input type="checkbox" name="visible_to_staff__{{ row.config.key }}" {% if row.config.visible_to_staff %}checked{% endif %} />
<span>{% trans "Staff" %}</span>
</label>
</div>
<p class="hint">{% trans "Wenn keine Firmenrolle aktiv ist, bleibt die App nur für die Platform sichtbar." %}</p>
</section>
<section class="app-registry-panel">
<h3>{% trans "Platzierung" %}</h3>
<div class="grid">
<div class="field">
<label for="section__{{ row.config.key }}">{% trans "Bereich" %}</label>
<select id="section__{{ row.config.key }}" name="section__{{ row.config.key }}">
{% for value, label in section_choices %}
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="sort_order__{{ row.config.key }}">{% trans "Reihenfolge" %}</label>
<input id="sort_order__{{ row.config.key }}" type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
</div>
</div>
</section>
<section class="app-registry-panel app-registry-copy-panel">
<h3>{% trans "Bezeichnungen & Texte" %}</h3>
<div class="grid lang-pairs">
<div class="lang-block">
<h4>{% trans "Deutsch" %}</h4>
<div class="field">
<label for="title_override__{{ row.config.key }}">{% trans "Titel" %}</label>
<input id="title_override__{{ row.config.key }}" type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
</div>
<div class="field">
<label for="description_override__{{ row.config.key }}">{% trans "Beschreibung" %}</label>
<textarea id="description_override__{{ row.config.key }}" name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
</div>
<div class="field">
<label for="action_label_override__{{ row.config.key }}">{% trans "Aktionslabel" %}</label>
<input id="action_label_override__{{ row.config.key }}" type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
</div>
</div>
<div class="lang-block">
<h4>{% trans "English" %}</h4>
<div class="field">
<label for="title_override_en__{{ row.config.key }}">{% trans "Title" %}</label>
<input id="title_override_en__{{ row.config.key }}" type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
</div>
<div class="field">
<label for="description_override_en__{{ row.config.key }}">{% trans "Description" %}</label>
<textarea id="description_override_en__{{ row.config.key }}" name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
</div>
<div class="field">
<label for="action_label_override_en__{{ row.config.key }}">{% trans "Action label" %}</label>
<input id="action_label_override_en__{{ row.config.key }}" type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
</div>
</div>
</div>
</section>
</div>
</details>
{% endfor %}
</div>
<div class="toolbar" style="margin-top:1rem;">
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
<div class="app-registry-savebar">
<div>
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
<div class="mini" id="app-registry-dirty-state">{% trans "Keine ungespeicherten Änderungen" %}</div>
</div>
<button class="btn btn-primary" type="submit">{% trans "App Registry speichern" %}</button>
</div>
</form>
</section>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const searchInput = document.getElementById('app-registry-search');
const stateSelect = document.getElementById('app-registry-state');
const sectionSelect = document.getElementById('app-registry-section');
const cards = Array.from(document.querySelectorAll('[data-app-card]'));
const form = document.querySelector('form[action$="app-registry/save/"], form[action*="save_portal_app_registry"]') || document.querySelector('.stack-form');
const dirtyState = document.getElementById('app-registry-dirty-state');
function applyFilters() {
const query = (searchInput?.value || '').trim().toLowerCase();
const state = stateSelect?.value || 'all';
const section = sectionSelect?.value || 'all';
cards.forEach((card) => {
const matchesQuery = !query || card.dataset.key.includes(query) || card.dataset.title.includes(query);
const matchesState =
state === 'all' ||
(state === 'enabled' && card.dataset.enabled === '1') ||
(state === 'disabled' && card.dataset.enabled === '0') ||
(state === 'platform_only' && card.dataset.platformOnly === '1');
const matchesSection = section === 'all' || card.dataset.section === section;
card.hidden = !(matchesQuery && matchesState && matchesSection);
});
}
function markDirty() {
if (!dirtyState) return;
dirtyState.textContent = "{{ dirty_state_label|escapejs }}";
}
searchInput?.addEventListener('input', applyFilters);
stateSelect?.addEventListener('change', applyFilters);
sectionSelect?.addEventListener('change', applyFilters);
form?.addEventListener('input', markDirty);
form?.addEventListener('change', markDirty);
applyFilters();
})();
</script>
{% endblock %}

View File

@@ -34,6 +34,7 @@
<span class="eyebrow">{% trans "Operations Console" %}</span>
<h1>{{ portal_title }}</h1>
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
{% if can_manage_integrations %}
<div class="status-row">
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {{ role_label }}</span>
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
@@ -44,6 +45,7 @@
</span>
<span class="status-pill status-pill-neutral">{% trans "PDF + E-Mail Workflow bereit" %}</span>
</div>
{% endif %}
</div>
</div>
</div>