snapshot: preserve scalable app registry and landing visibility rules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -27,11 +27,21 @@ class PortalBrandingAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(PortalAppConfig)
|
@admin.register(PortalAppConfig)
|
||||||
class PortalAppConfigAdmin(admin.ModelAdmin):
|
class PortalAppConfigAdmin(admin.ModelAdmin):
|
||||||
list_display = ('key', 'section', 'sort_order', 'is_enabled', 'updated_at')
|
list_display = (
|
||||||
list_filter = ('section', 'is_enabled')
|
'key',
|
||||||
|
'section',
|
||||||
|
'sort_order',
|
||||||
|
'is_enabled',
|
||||||
|
'visible_to_super_admin',
|
||||||
|
'visible_to_admin',
|
||||||
|
'visible_to_it_staff',
|
||||||
|
'visible_to_staff',
|
||||||
|
'updated_at',
|
||||||
|
)
|
||||||
|
list_filter = ('section', 'is_enabled', 'visible_to_super_admin', 'visible_to_admin', 'visible_to_it_staff', 'visible_to_staff')
|
||||||
search_fields = ('key', 'title_override', 'title_override_en')
|
search_fields = ('key', 'title_override', 'title_override_en')
|
||||||
ordering = ('section', 'sort_order', 'key')
|
ordering = ('section', 'sort_order', 'key')
|
||||||
list_editable = ('section', 'sort_order', 'is_enabled')
|
list_editable = ('section', 'sort_order', 'is_enabled', 'visible_to_super_admin', 'visible_to_admin', 'visible_to_it_staff', 'visible_to_staff')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(OnboardingRequest)
|
@admin.register(OnboardingRequest)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.urls import reverse
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import PortalAppConfig
|
from .models import PortalAppConfig
|
||||||
from .roles import user_has_capability
|
from .roles import ROLE_ADMIN, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, get_user_role_key, user_has_capability
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -160,6 +160,108 @@ APP_DEFINITIONS: tuple[AppDefinition, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ROLE_VISIBILITY = {
|
||||||
|
'onboarding': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: True,
|
||||||
|
ROLE_STAFF: True,
|
||||||
|
},
|
||||||
|
'offboarding': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: True,
|
||||||
|
ROLE_STAFF: True,
|
||||||
|
},
|
||||||
|
'requests_dashboard': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: True,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'branding': {
|
||||||
|
ROLE_SUPER_ADMIN: False,
|
||||||
|
ROLE_ADMIN: False,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'app_registry': {
|
||||||
|
ROLE_SUPER_ADMIN: False,
|
||||||
|
ROLE_ADMIN: False,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'integrations': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'users': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: False,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'audit_log': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'backups': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'welcome_emails': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'form_builder': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'intro_builder': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'handbook': {
|
||||||
|
ROLE_SUPER_ADMIN: True,
|
||||||
|
ROLE_ADMIN: True,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
'django_admin': {
|
||||||
|
ROLE_SUPER_ADMIN: False,
|
||||||
|
ROLE_ADMIN: False,
|
||||||
|
ROLE_IT_STAFF: False,
|
||||||
|
ROLE_STAFF: False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _default_visibility_summary(definition_key: str) -> str:
|
||||||
|
visibility = DEFAULT_ROLE_VISIBILITY.get(definition_key, {})
|
||||||
|
enabled_roles = [
|
||||||
|
role
|
||||||
|
for role in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF)
|
||||||
|
if visibility.get(role)
|
||||||
|
]
|
||||||
|
if not enabled_roles:
|
||||||
|
return str(_('Nur Platform'))
|
||||||
|
if enabled_roles == [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF]:
|
||||||
|
return str(_('Alle Firmenrollen'))
|
||||||
|
return ' + '.join(str(ROLE_LABELS[role]) for role in enabled_roles if role in ROLE_LABELS)
|
||||||
|
|
||||||
|
|
||||||
SECTION_META = {
|
SECTION_META = {
|
||||||
PortalAppConfig.SECTION_APP: {
|
PortalAppConfig.SECTION_APP: {
|
||||||
'title': _('Apps'),
|
'title': _('Apps'),
|
||||||
@@ -184,12 +286,17 @@ SECTION_META = {
|
|||||||
|
|
||||||
def ensure_portal_app_configs() -> None:
|
def ensure_portal_app_configs() -> None:
|
||||||
for index, definition in enumerate(APP_DEFINITIONS):
|
for index, definition in enumerate(APP_DEFINITIONS):
|
||||||
|
visibility = DEFAULT_ROLE_VISIBILITY.get(definition.key, {})
|
||||||
PortalAppConfig.objects.get_or_create(
|
PortalAppConfig.objects.get_or_create(
|
||||||
key=definition.key,
|
key=definition.key,
|
||||||
defaults={
|
defaults={
|
||||||
'section': definition.section,
|
'section': definition.section,
|
||||||
'sort_order': index,
|
'sort_order': index,
|
||||||
'is_enabled': True,
|
'is_enabled': True,
|
||||||
|
'visible_to_super_admin': visibility.get(ROLE_SUPER_ADMIN, False),
|
||||||
|
'visible_to_admin': visibility.get(ROLE_ADMIN, False),
|
||||||
|
'visible_to_it_staff': visibility.get(ROLE_IT_STAFF, False),
|
||||||
|
'visible_to_staff': visibility.get(ROLE_STAFF, False),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,6 +313,7 @@ def get_portal_app_registry_rows() -> list[dict[str, object]]:
|
|||||||
'config': config,
|
'config': config,
|
||||||
'default_section': definition.section,
|
'default_section': definition.section,
|
||||||
'default_sort_order': index,
|
'default_sort_order': index,
|
||||||
|
'default_visibility_summary': _default_visibility_summary(definition.key),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
@@ -215,6 +323,7 @@ def build_portal_app_sections(user) -> list[dict[str, object]]:
|
|||||||
ensure_portal_app_configs()
|
ensure_portal_app_configs()
|
||||||
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
|
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
|
||||||
grouped: dict[str, list[dict[str, object]]] = {key: [] for key in SECTION_META}
|
grouped: dict[str, list[dict[str, object]]] = {key: [] for key in SECTION_META}
|
||||||
|
role_key = get_user_role_key(user)
|
||||||
|
|
||||||
for definition in APP_DEFINITIONS:
|
for definition in APP_DEFINITIONS:
|
||||||
config = config_map.get(definition.key)
|
config = config_map.get(definition.key)
|
||||||
@@ -222,6 +331,15 @@ def build_portal_app_sections(user) -> list[dict[str, object]]:
|
|||||||
continue
|
continue
|
||||||
if definition.capability and not user_has_capability(user, definition.capability):
|
if definition.capability and not user_has_capability(user, definition.capability):
|
||||||
continue
|
continue
|
||||||
|
if role_key != ROLE_PLATFORM_OWNER:
|
||||||
|
if role_key == ROLE_SUPER_ADMIN and not config.visible_to_super_admin:
|
||||||
|
continue
|
||||||
|
if role_key == ROLE_ADMIN and not config.visible_to_admin:
|
||||||
|
continue
|
||||||
|
if role_key == ROLE_IT_STAFF and not config.visible_to_it_staff:
|
||||||
|
continue
|
||||||
|
if role_key == ROLE_STAFF and not config.visible_to_staff:
|
||||||
|
continue
|
||||||
grouped[config.section].append(
|
grouped[config.section].append(
|
||||||
{
|
{
|
||||||
'key': definition.key,
|
'key': definition.key,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2026-03-26 11:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('workflows', '0040_portalbranding_favicon_image_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='portalappconfig',
|
||||||
|
name='visible_to_admin',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='portalappconfig',
|
||||||
|
name='visible_to_it_staff',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='portalappconfig',
|
||||||
|
name='visible_to_staff',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='portalappconfig',
|
||||||
|
name='visible_to_super_admin',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -86,6 +86,10 @@ class PortalAppConfig(models.Model):
|
|||||||
section = models.CharField(max_length=20, choices=SECTION_CHOICES, default=SECTION_APP)
|
section = models.CharField(max_length=20, choices=SECTION_CHOICES, default=SECTION_APP)
|
||||||
sort_order = models.PositiveIntegerField(default=0)
|
sort_order = models.PositiveIntegerField(default=0)
|
||||||
is_enabled = models.BooleanField(default=True)
|
is_enabled = models.BooleanField(default=True)
|
||||||
|
visible_to_super_admin = models.BooleanField(default=True)
|
||||||
|
visible_to_admin = models.BooleanField(default=True)
|
||||||
|
visible_to_it_staff = models.BooleanField(default=False)
|
||||||
|
visible_to_staff = models.BooleanField(default=False)
|
||||||
title_override = models.CharField(max_length=255, blank=True)
|
title_override = models.CharField(max_length=255, blank=True)
|
||||||
title_override_en = models.CharField(max_length=255, blank=True)
|
title_override_en = models.CharField(max_length=255, blank=True)
|
||||||
description_override = models.TextField(blank=True)
|
description_override = models.TextField(blank=True)
|
||||||
|
|||||||
@@ -65,6 +65,30 @@ th { background: #f6f9ff; color: #334155; }
|
|||||||
.app-registry-table select,
|
.app-registry-table select,
|
||||||
.app-registry-table textarea { min-width: 160px; }
|
.app-registry-table textarea { min-width: 160px; }
|
||||||
.app-registry-table td { background: rgba(255,255,255,0.9); }
|
.app-registry-table td { background: rgba(255,255,255,0.9); }
|
||||||
|
.app-registry-cards { display: grid; gap: 14px; }
|
||||||
|
.app-registry-filters { display: grid; grid-template-columns: minmax(260px, 1.5fr) repeat(2, minmax(180px, 0.7fr)); gap: 12px; margin-bottom: 14px; }
|
||||||
|
.app-registry-card { border: 1px solid #d9e4f1; border-radius: 18px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,250,255,0.95)); padding: 16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.94); transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 180ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
||||||
|
.app-registry-card:hover { transform: translateY(-1px); box-shadow: 0 12px 24px rgba(16, 32, 57, 0.06); border-color: #c9d8eb; }
|
||||||
|
.app-registry-card.is-disabled { opacity: 0.84; }
|
||||||
|
.app-registry-card[hidden] { display: none !important; }
|
||||||
|
.app-registry-card-head { display: flex; justify-content: space-between; align-items: start; gap: 14px; margin-bottom: 14px; }
|
||||||
|
.app-registry-card-title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
|
||||||
|
.app-registry-card-title-row h2 { margin: 0; color: #17345e; font-size: 19px; }
|
||||||
|
.app-registry-card-copy { margin: 8px 0 0; color: #60738d; max-width: 760px; }
|
||||||
|
.app-registry-summary { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.9fr); gap: 16px; align-items: center; list-style: none; cursor: pointer; }
|
||||||
|
.app-registry-summary::-webkit-details-marker { display: none; }
|
||||||
|
.app-registry-summary::marker { display: none; }
|
||||||
|
.app-registry-summary-main { min-width: 0; }
|
||||||
|
.app-registry-summary-meta { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; align-items: center; }
|
||||||
|
.app-registry-card-grid { display: grid; grid-template-columns: repeat(2, minmax(260px, 1fr)); gap: 12px; align-items: start; }
|
||||||
|
.app-registry-card .app-registry-card-grid { margin-top: 14px; padding-top: 14px; border-top: 1px solid #dce6f2; }
|
||||||
|
.app-registry-panel { border: 1px solid #dce6f2; border-radius: 14px; background: rgba(255,255,255,0.86); padding: 12px; }
|
||||||
|
.app-registry-panel h3 { margin: 0 0 10px; color: #213a61; font-size: 15px; }
|
||||||
|
.app-registry-panel h4 { margin: 0 0 10px; color: #223b63; font-size: 14px; }
|
||||||
|
.app-registry-checks { gap: 10px 14px; }
|
||||||
|
.app-registry-checks label { min-width: 130px; padding: 8px 10px; border: 1px solid #d7e2ef; border-radius: 12px; background: #f8fbff; }
|
||||||
|
.app-registry-copy-panel { grid-column: 1 / -1; }
|
||||||
|
.app-registry-savebar { position: sticky; bottom: 14px; z-index: 5; display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 16px; padding: 12px 14px; border: 1px solid #cad8ea; border-radius: 16px; background: rgba(255,255,255,0.95); box-shadow: 0 12px 24px rgba(16, 32, 57, 0.08); backdrop-filter: blur(8px); }
|
||||||
.template-block { border: 1px solid #d8e3f0; border-radius: 10px; background: #fff; padding: 10px; margin-top: 10px; }
|
.template-block { border: 1px solid #d8e3f0; border-radius: 10px; background: #fff; padding: 10px; margin-top: 10px; }
|
||||||
.template-title, .rule-title { margin: 0 0 8px; color: #24344e; font-weight: 700; font-size: 14px; }
|
.template-title, .rule-title { margin: 0 0 8px; color: #24344e; font-weight: 700; font-size: 14px; }
|
||||||
.rule-card { margin-top: 12px; border: 1px solid #d8e3f0; border-radius: 12px; padding: 10px; background: #fff; }
|
.rule-card { margin-top: 12px; border: 1px solid #d8e3f0; border-radius: 12px; padding: 10px; background: #fff; }
|
||||||
@@ -89,4 +113,10 @@ th { background: #f6f9ff; color: #334155; }
|
|||||||
.grid { grid-template-columns: 1fr; }
|
.grid { grid-template-columns: 1fr; }
|
||||||
.branding-preview-header { flex-direction: column; align-items: flex-start; }
|
.branding-preview-header { flex-direction: column; align-items: flex-start; }
|
||||||
.branding-preview-band { flex-wrap: wrap; }
|
.branding-preview-band { flex-wrap: wrap; }
|
||||||
|
.app-registry-filters { grid-template-columns: 1fr; }
|
||||||
|
.app-registry-summary { grid-template-columns: 1fr; }
|
||||||
|
.app-registry-summary-meta { justify-content: flex-start; }
|
||||||
|
.app-registry-card-grid { grid-template-columns: 1fr; }
|
||||||
|
.app-registry-copy-panel { grid-column: auto; }
|
||||||
|
.app-registry-savebar { align-items: stretch; flex-direction: column; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'workflows/base_shell.html' %}
|
{% extends 'workflows/base_shell.html' %}
|
||||||
{% load static i18n %}
|
{% load static i18n %}
|
||||||
|
{% trans "Ungespeicherte Änderungen" as dirty_state_label %}
|
||||||
|
|
||||||
{% block title %}{% trans "App Registry" %}{% endblock %}
|
{% block title %}{% trans "App Registry" %}{% endblock %}
|
||||||
|
|
||||||
@@ -21,63 +22,220 @@
|
|||||||
</div>
|
</div>
|
||||||
<form method="post" action="{% url 'save_portal_app_registry' %}" class="stack-form">
|
<form method="post" action="{% url 'save_portal_app_registry' %}" class="stack-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-wrap app-registry-wrap">
|
<section class="app-registry-filters">
|
||||||
<table class="app-registry-table">
|
<div class="field">
|
||||||
<thead>
|
<label for="app-registry-search">{% trans "Suche" %}</label>
|
||||||
<tr>
|
<input id="app-registry-search" type="text" placeholder="{% trans 'Nach App-Name oder Key filtern' %}" />
|
||||||
<th>{% trans "Key" %}</th>
|
</div>
|
||||||
<th>{% trans "Aktiv" %}</th>
|
<div class="field">
|
||||||
<th>{% trans "Bereich" %}</th>
|
<label for="app-registry-state">{% trans "Status" %}</label>
|
||||||
<th>{% trans "Reihenfolge" %}</th>
|
<select id="app-registry-state">
|
||||||
<th>{% trans "Titel DE" %}</th>
|
<option value="all">{% trans "Alle" %}</option>
|
||||||
<th>{% trans "Titel EN" %}</th>
|
<option value="enabled">{% trans "Aktiv" %}</option>
|
||||||
<th>{% trans "Aktion DE" %}</th>
|
<option value="disabled">{% trans "Deaktiviert" %}</option>
|
||||||
<th>{% trans "Aktion EN" %}</th>
|
<option value="platform_only">{% trans "Platform only" %}</option>
|
||||||
</tr>
|
</select>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div class="field">
|
||||||
{% for row in rows %}
|
<label for="app-registry-section">{% trans "Bereich" %}</label>
|
||||||
<tr>
|
<select id="app-registry-section">
|
||||||
<td>
|
<option value="all">{% trans "Alle" %}</option>
|
||||||
<div><strong>{{ row.config.key }}</strong></div>
|
<option value="apps">{% trans "Apps" %}</option>
|
||||||
<div class="mini">{{ row.definition.title }}</div>
|
<option value="platform_apps">{% trans "Platform Apps" %}</option>
|
||||||
</td>
|
<option value="admin_apps">{% trans "Admin Apps" %}</option>
|
||||||
<td class="select-col">
|
</select>
|
||||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
</div>
|
||||||
</td>
|
</section>
|
||||||
<td>
|
<div class="app-registry-cards">
|
||||||
<select name="section__{{ row.config.key }}">
|
{% for row in rows %}
|
||||||
{% for value, label in section_choices %}
|
<details class="app-registry-card{% if not row.config.is_enabled %} is-disabled{% endif %}" data-app-card
|
||||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
data-key="{{ row.config.key|lower }}"
|
||||||
{% endfor %}
|
data-title="{{ row.definition.title|lower }}"
|
||||||
</select>
|
data-enabled="{% if row.config.is_enabled %}1{% else %}0{% endif %}"
|
||||||
</td>
|
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 %}"
|
||||||
<td>
|
data-section="{{ row.config.section }}">
|
||||||
<input type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
|
<summary class="app-registry-summary">
|
||||||
</td>
|
<div class="app-registry-summary-main">
|
||||||
<td>
|
<div class="app-registry-card-title-row">
|
||||||
<input type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
<h2>{{ row.definition.title }}</h2>
|
||||||
<textarea name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
{% if row.config.is_enabled %}
|
||||||
</td>
|
<span class="badge sent">{% trans "Aktiv" %}</span>
|
||||||
<td>
|
{% else %}
|
||||||
<input type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
|
||||||
<textarea name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
{% endif %}
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="mini">{{ row.config.key }}</div>
|
||||||
<input type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
<p class="app-registry-card-copy">{{ row.definition.description }}</p>
|
||||||
</td>
|
<p class="mini">{% trans "Empfohlener Standardzugriff:" %} {{ row.default_visibility_summary }}</p>
|
||||||
<td>
|
</div>
|
||||||
<input type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
<div class="app-registry-summary-meta">
|
||||||
</td>
|
<span class="badge scheduled">
|
||||||
</tr>
|
{% if row.config.section == 'platform_apps' %}
|
||||||
{% endfor %}
|
{% trans "Platform Apps" %}
|
||||||
</tbody>
|
{% elif row.config.section == 'admin_apps' %}
|
||||||
</table>
|
{% 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>
|
||||||
<div class="toolbar" style="margin-top:1rem;">
|
<div class="app-registry-savebar">
|
||||||
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
|
<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>
|
<button class="btn btn-primary" type="submit">{% trans "App Registry speichern" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<span class="eyebrow">{% trans "Operations Console" %}</span>
|
<span class="eyebrow">{% trans "Operations Console" %}</span>
|
||||||
<h1>{{ portal_title }}</h1>
|
<h1>{{ portal_title }}</h1>
|
||||||
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
|
<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">
|
<div class="status-row">
|
||||||
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {{ role_label }}</span>
|
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {{ role_label }}</span>
|
||||||
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="status-pill status-pill-neutral">{% trans "PDF + E-Mail Workflow bereit" %}</span>
|
<span class="status-pill status-pill-neutral">{% trans "PDF + E-Mail Workflow bereit" %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str])
|
|||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
|
role_key = get_user_role_key(request.user)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'workflows/home.html',
|
'workflows/home.html',
|
||||||
@@ -336,6 +337,7 @@ def home(request):
|
|||||||
'email_test_mode': is_email_test_mode(),
|
'email_test_mode': is_email_test_mode(),
|
||||||
'workflow_config': config,
|
'workflow_config': config,
|
||||||
'role_label': get_user_role_label(request.user),
|
'role_label': get_user_role_label(request.user),
|
||||||
|
'role_key': role_key,
|
||||||
'portal_app_sections': build_portal_app_sections(request.user),
|
'portal_app_sections': build_portal_app_sections(request.user),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -364,6 +366,10 @@ def save_portal_app_registry(request):
|
|||||||
if config.section not in dict(PortalAppConfig.SECTION_CHOICES):
|
if config.section not in dict(PortalAppConfig.SECTION_CHOICES):
|
||||||
config.section = row['default_section']
|
config.section = row['default_section']
|
||||||
config.is_enabled = request.POST.get(f'is_enabled__{key}') == 'on'
|
config.is_enabled = request.POST.get(f'is_enabled__{key}') == 'on'
|
||||||
|
config.visible_to_super_admin = request.POST.get(f'visible_to_super_admin__{key}') == 'on'
|
||||||
|
config.visible_to_admin = request.POST.get(f'visible_to_admin__{key}') == 'on'
|
||||||
|
config.visible_to_it_staff = request.POST.get(f'visible_to_it_staff__{key}') == 'on'
|
||||||
|
config.visible_to_staff = request.POST.get(f'visible_to_staff__{key}') == 'on'
|
||||||
try:
|
try:
|
||||||
config.sort_order = int((request.POST.get(f'sort_order__{key}') or '').strip() or row['default_sort_order'])
|
config.sort_order = int((request.POST.get(f'sort_order__{key}') or '').strip() or row['default_sort_order'])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
Reference in New Issue
Block a user