snapshot: preserve custom field parity across forms timeline and pdf
This commit is contained in:
@@ -10,6 +10,7 @@ from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model, login as auth_login
|
||||
@@ -46,15 +47,18 @@ from .form_builder import (
|
||||
ONBOARDING_DEFAULT_PAGE,
|
||||
ONBOARDING_PAGE_LABELS,
|
||||
ONBOARDING_PAGE_ORDER,
|
||||
build_custom_field_key,
|
||||
custom_field_target_key,
|
||||
ensure_form_field_configs,
|
||||
ensure_form_conditional_rule_configs,
|
||||
ensure_form_section_configs,
|
||||
get_custom_field_configs,
|
||||
get_default_page_map,
|
||||
get_section_labels,
|
||||
get_section_order,
|
||||
apply_form_preset,
|
||||
)
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
||||
from .emailing import send_system_email
|
||||
from .notifications import notify_user
|
||||
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
|
||||
@@ -105,8 +109,6 @@ ONBOARDING_GROUPS = {
|
||||
'phone-box': ['phone_number_choice'],
|
||||
}
|
||||
|
||||
ONBOARDING_HIDDEN_BY_DEFAULT = set(DEFAULT_CONDITIONAL_RULES.get('onboarding', {}).keys())
|
||||
|
||||
ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'}
|
||||
ONBOARDING_CHECKBOX_LISTS = {
|
||||
'needed_devices_multi',
|
||||
@@ -144,6 +146,10 @@ def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]:
|
||||
return payload
|
||||
|
||||
|
||||
def _active_conditional_target_keys(form_type: str) -> set[str]:
|
||||
return set(_normalized_conditional_rule_payload(form_type).keys())
|
||||
|
||||
|
||||
def healthz(request):
|
||||
db_ok = True
|
||||
try:
|
||||
@@ -450,6 +456,36 @@ def _request_status_label(status_key: str, language_code: str | None = None) ->
|
||||
return labels.get(status_key, status_key)
|
||||
|
||||
|
||||
def _request_custom_field_details(obj, kind: str, language_code: str | None = None) -> list[dict[str, str]]:
|
||||
form_type = 'onboarding' if kind == 'onboarding' else 'offboarding'
|
||||
language_code = ((language_code or getattr(obj, 'preferred_language', '') or get_language() or 'de').split('-')[0]).lower()
|
||||
values = getattr(obj, 'custom_field_values', {}) or {}
|
||||
rows = []
|
||||
yes_label = 'Ja' if language_code == 'de' else 'Yes'
|
||||
for cfg in get_custom_field_configs(form_type, include_inactive=True):
|
||||
raw_value = values.get(cfg.field_key)
|
||||
if raw_value in (None, '', False, []):
|
||||
continue
|
||||
if isinstance(raw_value, bool):
|
||||
display_value = str(yes_label) if raw_value else ''
|
||||
elif isinstance(raw_value, list):
|
||||
display_value = ', '.join(str(item).strip() for item in raw_value if str(item).strip())
|
||||
else:
|
||||
display_value = str(raw_value).strip()
|
||||
if not display_value:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
'label': cfg.translated_label(language_code),
|
||||
'value': display_value,
|
||||
'section': cfg.section_key,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (item['section'], item['sort_order'], item['label']))
|
||||
return rows
|
||||
|
||||
|
||||
def _audit_action_label(action: str) -> str:
|
||||
labels = {
|
||||
'requests_deleted': _('Vorgänge gelöscht'),
|
||||
@@ -505,6 +541,7 @@ def _build_onboarding_layout(form) -> list[dict]:
|
||||
for group_id, group_fields in ONBOARDING_GROUPS.items():
|
||||
for name in group_fields:
|
||||
group_by_field[name] = group_id
|
||||
conditional_target_keys = _active_conditional_target_keys('onboarding')
|
||||
|
||||
rendered_groups = set()
|
||||
consumed = set()
|
||||
@@ -529,7 +566,7 @@ def _build_onboarding_layout(form) -> list[dict]:
|
||||
{
|
||||
'kind': 'group',
|
||||
'id': group_id,
|
||||
'hidden_default': group_id in ONBOARDING_HIDDEN_BY_DEFAULT,
|
||||
'hidden_default': group_id in conditional_target_keys,
|
||||
'fields': group_fields,
|
||||
}
|
||||
)
|
||||
@@ -537,6 +574,18 @@ def _build_onboarding_layout(form) -> list[dict]:
|
||||
consumed.update([f.name for f in group_fields])
|
||||
continue
|
||||
|
||||
if field_name.startswith('custom__') and field_name in conditional_target_keys:
|
||||
blocks.append(
|
||||
{
|
||||
'kind': 'group',
|
||||
'id': field_name,
|
||||
'hidden_default': True,
|
||||
'fields': [form[field_name]],
|
||||
}
|
||||
)
|
||||
consumed.add(field_name)
|
||||
continue
|
||||
|
||||
blocks.append({'kind': 'field', 'field': form[field_name]})
|
||||
consumed.add(field_name)
|
||||
|
||||
@@ -600,10 +649,42 @@ def _build_offboarding_sections(form, visible_section_keys: set[str] | None = No
|
||||
]
|
||||
|
||||
|
||||
def _ops_summary_for_user(user) -> dict[str, object]:
|
||||
can_view_jobs = user_has_capability(user, 'view_job_monitor')
|
||||
can_manage_backups = user_has_capability(user, 'manage_backups')
|
||||
summary: dict[str, object] = {
|
||||
'show': can_view_jobs or can_manage_backups,
|
||||
'can_view_jobs': can_view_jobs,
|
||||
'can_manage_backups': can_manage_backups,
|
||||
'failed_count_24h': 0,
|
||||
'started_count_24h': 0,
|
||||
'success_count_24h': 0,
|
||||
'recent_failed_logs': [],
|
||||
'backup_health': latest_backup_health_snapshot() if can_manage_backups else None,
|
||||
}
|
||||
if not can_view_jobs:
|
||||
return summary
|
||||
|
||||
since = timezone.now() - timedelta(hours=24)
|
||||
logs = AsyncTaskLog.objects.filter(started_at__gte=since)
|
||||
counts = {
|
||||
row['status']: row['count']
|
||||
for row in logs.values('status').annotate(count=Count('id'))
|
||||
}
|
||||
summary['failed_count_24h'] = counts.get('failed', 0)
|
||||
summary['started_count_24h'] = counts.get('started', 0)
|
||||
summary['success_count_24h'] = counts.get('succeeded', 0)
|
||||
summary['recent_failed_logs'] = list(
|
||||
AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5]
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||
role_key = get_user_role_key(request.user)
|
||||
ops_summary = _ops_summary_for_user(request.user)
|
||||
return render(
|
||||
request,
|
||||
'workflows/home.html',
|
||||
@@ -614,6 +695,7 @@ def home(request):
|
||||
'role_label': get_user_role_label(request.user),
|
||||
'role_key': role_key,
|
||||
'portal_app_sections': build_portal_app_sections(request.user),
|
||||
'ops_summary': ops_summary,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -641,6 +723,13 @@ def job_monitor_page(request):
|
||||
logs = logs.filter(task_name=task_filter)
|
||||
logs = logs.order_by('-started_at', '-id')[:200]
|
||||
task_names = list(AsyncTaskLog.objects.order_by('task_name').values_list('task_name', flat=True).distinct())
|
||||
since = timezone.now() - timedelta(hours=24)
|
||||
recent_logs = AsyncTaskLog.objects.filter(started_at__gte=since)
|
||||
counts = {
|
||||
row['status']: row['count']
|
||||
for row in recent_logs.values('status').annotate(count=Count('id'))
|
||||
}
|
||||
recent_failed = list(AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5])
|
||||
return render(
|
||||
request,
|
||||
'workflows/job_monitor.html',
|
||||
@@ -650,6 +739,12 @@ def job_monitor_page(request):
|
||||
'task_filter': task_filter,
|
||||
'task_names': task_names,
|
||||
'status_choices': [('started', _('Gestartet')), ('succeeded', _('Erfolgreich')), ('failed', _('Fehlgeschlagen'))],
|
||||
'job_summary': {
|
||||
'started_count_24h': counts.get('started', 0),
|
||||
'success_count_24h': counts.get('succeeded', 0),
|
||||
'failed_count_24h': counts.get('failed', 0),
|
||||
'recent_failed': recent_failed,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1469,6 +1564,7 @@ def request_timeline_page(request, kind: str, request_id: int):
|
||||
return redirect('requests_dashboard')
|
||||
|
||||
request_label = _request_target_label(obj, kind)
|
||||
custom_field_details = _request_custom_field_details(obj, kind, getattr(request, 'LANGUAGE_CODE', None))
|
||||
audit_rows = list(
|
||||
AdminAuditLog.objects.select_related('actor')
|
||||
.filter(target_type__in=[kind, 'request'])
|
||||
@@ -1483,6 +1579,7 @@ def request_timeline_page(request, kind: str, request_id: int):
|
||||
'title': _('Anfrage erstellt'),
|
||||
'summary': request_label,
|
||||
'meta': _('Status: %(status)s') % {'status': obj.get_processing_status_display()},
|
||||
'details': {item['label']: item['value'] for item in custom_field_details},
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1569,6 +1666,7 @@ def request_timeline_page(request, kind: str, request_id: int):
|
||||
'request_obj': obj,
|
||||
'request_label': request_label,
|
||||
'timeline_rows': timeline_rows,
|
||||
'custom_field_details': custom_field_details,
|
||||
'contract_start': getattr(obj, 'contract_start', None),
|
||||
'handover_date': getattr(obj, 'handover_date', None),
|
||||
},
|
||||
@@ -2076,6 +2174,7 @@ def form_builder_page(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
delete_option_id = request.POST.get('delete_option_id', '').strip()
|
||||
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
|
||||
if delete_option_id:
|
||||
option = FormOption.objects.filter(id=delete_option_id).first()
|
||||
if not option:
|
||||
@@ -2088,6 +2187,17 @@ def form_builder_page(request):
|
||||
_audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label)
|
||||
messages.success(request, 'Option wurde gelöscht.')
|
||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=options#builder-content")
|
||||
if delete_custom_field_id:
|
||||
custom_field = FormCustomFieldConfig.objects.filter(id=delete_custom_field_id, form_type=form_type).first()
|
||||
if not custom_field:
|
||||
messages.error(request, 'Benutzerdefiniertes Feld nicht gefunden.')
|
||||
else:
|
||||
deleted_label = custom_field.label
|
||||
deleted_id = custom_field.id
|
||||
custom_field.delete()
|
||||
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
|
||||
messages.success(request, 'Benutzerdefiniertes Feld wurde gelöscht.')
|
||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
|
||||
|
||||
action = request.POST.get('builder_action', '')
|
||||
if action == 'add_option':
|
||||
@@ -2157,6 +2267,91 @@ def form_builder_page(request):
|
||||
_audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)})
|
||||
messages.success(request, 'Feldtexte wurden gespeichert.')
|
||||
|
||||
elif action == 'add_custom_field':
|
||||
label = (request.POST.get('custom_label') or '').strip()
|
||||
label_en = (request.POST.get('custom_label_en') or '').strip()
|
||||
section_key = (request.POST.get('custom_section_key') or '').strip()
|
||||
field_type = (request.POST.get('custom_field_type') or '').strip()
|
||||
sort_order_raw = (request.POST.get('custom_sort_order') or '').strip()
|
||||
help_text = (request.POST.get('custom_help_text') or '').strip()
|
||||
help_text_en = (request.POST.get('custom_help_text_en') or '').strip()
|
||||
select_options = (request.POST.get('custom_select_options') or '').strip()
|
||||
select_options_en = (request.POST.get('custom_select_options_en') or '').strip()
|
||||
section_choices = {key for key in get_section_order(form_type)}
|
||||
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
|
||||
if not label:
|
||||
messages.error(request, 'Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.')
|
||||
elif section_key not in section_choices:
|
||||
messages.error(request, 'Ungültiger Abschnitt für das benutzerdefinierte Feld.')
|
||||
elif field_type not in field_type_choices:
|
||||
messages.error(request, 'Ungültiger Feldtyp.')
|
||||
elif field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not select_options:
|
||||
messages.error(request, 'Auswahlfelder benötigen mindestens eine Option.')
|
||||
else:
|
||||
field_key_base = build_custom_field_key(label)
|
||||
field_key = field_key_base
|
||||
suffix = 2
|
||||
while FormCustomFieldConfig.objects.filter(form_type=form_type, field_key=field_key).exists():
|
||||
field_key = f'{field_key_base}_{suffix}'
|
||||
suffix += 1
|
||||
try:
|
||||
sort_order = int(sort_order_raw or 0)
|
||||
except ValueError:
|
||||
sort_order = 0
|
||||
FormCustomFieldConfig.objects.create(
|
||||
form_type=form_type,
|
||||
field_key=field_key,
|
||||
section_key=section_key,
|
||||
sort_order=max(0, sort_order),
|
||||
field_type=field_type,
|
||||
is_active=True,
|
||||
is_required=request.POST.get('custom_is_required') == 'on',
|
||||
label=label,
|
||||
label_en=label_en,
|
||||
help_text=help_text,
|
||||
help_text_en=help_text_en,
|
||||
select_options=select_options,
|
||||
select_options_en=select_options_en,
|
||||
)
|
||||
_audit(request, 'form_custom_field_added', target_type='form_custom_field', target_label=label, details={'form_type': form_type, 'field_type': field_type, 'section_key': section_key})
|
||||
messages.success(request, 'Benutzerdefiniertes Feld wurde hinzugefügt.')
|
||||
|
||||
elif action == 'save_custom_fields':
|
||||
custom_ids = request.POST.getlist('custom_field_ids')
|
||||
updated = 0
|
||||
section_choices = {key for key in get_section_order(form_type)}
|
||||
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
|
||||
for raw_id in custom_ids:
|
||||
cfg = FormCustomFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
|
||||
if not cfg:
|
||||
continue
|
||||
field_type = (request.POST.get(f'custom_field_type_{cfg.id}') or '').strip()
|
||||
section_key = (request.POST.get(f'custom_section_key_{cfg.id}') or '').strip()
|
||||
try:
|
||||
sort_order = int((request.POST.get(f'custom_sort_order_{cfg.id}') or '').strip() or cfg.sort_order)
|
||||
except ValueError:
|
||||
sort_order = cfg.sort_order
|
||||
cfg.label = (request.POST.get(f'custom_label_{cfg.id}') or '').strip() or cfg.label
|
||||
cfg.label_en = (request.POST.get(f'custom_label_en_{cfg.id}') or '').strip()
|
||||
cfg.help_text = (request.POST.get(f'custom_help_text_{cfg.id}') or '').strip()
|
||||
cfg.help_text_en = (request.POST.get(f'custom_help_text_en_{cfg.id}') or '').strip()
|
||||
cfg.is_required = request.POST.get(f'custom_is_required_{cfg.id}') == 'on'
|
||||
cfg.is_active = request.POST.get(f'custom_is_active_{cfg.id}') == 'on'
|
||||
if field_type in field_type_choices:
|
||||
cfg.field_type = field_type
|
||||
if section_key in section_choices:
|
||||
cfg.section_key = section_key
|
||||
cfg.sort_order = max(0, sort_order)
|
||||
cfg.select_options = (request.POST.get(f'custom_select_options_{cfg.id}') or '').strip()
|
||||
cfg.select_options_en = (request.POST.get(f'custom_select_options_en_{cfg.id}') or '').strip()
|
||||
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not cfg.select_options:
|
||||
messages.error(request, f'Auswahlfeld "{cfg.label}" benötigt mindestens eine Option.')
|
||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
|
||||
cfg.save()
|
||||
updated += 1
|
||||
_audit(request, 'form_custom_fields_saved', target_type='form_custom_field', target_label=form_type, details={'count': updated})
|
||||
messages.success(request, 'Benutzerdefinierte Felder wurden gespeichert.')
|
||||
|
||||
elif action == 'save_field_rules':
|
||||
field_ids = request.POST.getlist('field_rule_ids')
|
||||
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
|
||||
@@ -2223,12 +2418,14 @@ def form_builder_page(request):
|
||||
else:
|
||||
messages.error(request, 'Preset konnte nicht angewendet werden.')
|
||||
|
||||
if action in {'add_option', 'save_options', 'save_field_texts'}:
|
||||
if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields'}:
|
||||
active_panel = 'builder-content'
|
||||
if action in {'add_option', 'save_options'}:
|
||||
active_subpanel = 'options'
|
||||
elif action == 'save_field_texts':
|
||||
active_subpanel = 'field-texts'
|
||||
elif action in {'add_custom_field', 'save_custom_fields'}:
|
||||
active_subpanel = 'custom-fields'
|
||||
elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}:
|
||||
active_panel = 'builder-rules'
|
||||
redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}"
|
||||
@@ -2263,6 +2460,7 @@ def form_builder_page(request):
|
||||
labels = _form_field_labels(form_type)
|
||||
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
||||
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||
custom_field_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('section_key', 'sort_order', 'field_key'))
|
||||
|
||||
if form_type == 'onboarding':
|
||||
columns = [
|
||||
@@ -2289,8 +2487,30 @@ def form_builder_page(request):
|
||||
'is_required': cfg.is_required,
|
||||
'locked': cfg.field_name in locked,
|
||||
'page_key': page_key,
|
||||
'is_custom': False,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
for cfg in custom_field_configs:
|
||||
page_key = cfg.section_key or fallback
|
||||
if page_key not in column_by_key:
|
||||
page_key = fallback
|
||||
column_by_key[page_key]['items'].append(
|
||||
{
|
||||
'field_name': f'custom__{cfg.field_key}',
|
||||
'label': cfg.translated_label(language_code),
|
||||
'label_de': cfg.label,
|
||||
'label_en': cfg.label_en,
|
||||
'is_visible': cfg.is_active,
|
||||
'is_required': cfg.is_required,
|
||||
'locked': False,
|
||||
'page_key': page_key,
|
||||
'is_custom': True,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
for column in columns:
|
||||
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
|
||||
else:
|
||||
columns = [
|
||||
{
|
||||
@@ -2316,8 +2536,30 @@ def form_builder_page(request):
|
||||
'is_required': cfg.is_required,
|
||||
'locked': cfg.field_name in locked,
|
||||
'page_key': page_key,
|
||||
'is_custom': False,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
for cfg in custom_field_configs:
|
||||
page_key = cfg.section_key or fallback
|
||||
if page_key not in column_by_key:
|
||||
page_key = fallback
|
||||
column_by_key[page_key]['items'].append(
|
||||
{
|
||||
'field_name': f'custom__{cfg.field_key}',
|
||||
'label': cfg.translated_label(language_code),
|
||||
'label_de': cfg.label,
|
||||
'label_en': cfg.label_en,
|
||||
'is_visible': cfg.is_active,
|
||||
'is_required': cfg.is_required,
|
||||
'locked': False,
|
||||
'page_key': page_key,
|
||||
'is_custom': True,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
for column in columns:
|
||||
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
|
||||
|
||||
section_rule_items = []
|
||||
if section_order:
|
||||
@@ -2350,6 +2592,20 @@ def form_builder_page(request):
|
||||
}
|
||||
)
|
||||
|
||||
custom_field_groups = []
|
||||
if section_order:
|
||||
grouped_custom = {key: [] for key in section_order}
|
||||
for cfg in custom_field_configs:
|
||||
grouped_custom.setdefault(cfg.section_key, []).append(cfg)
|
||||
for key in section_order:
|
||||
custom_field_groups.append(
|
||||
{
|
||||
'key': key,
|
||||
'title': section_labels.get(key, key),
|
||||
'items': grouped_custom.get(key, []),
|
||||
}
|
||||
)
|
||||
|
||||
field_rule_groups = []
|
||||
if section_order:
|
||||
grouped_rules = {key: [] for key in section_order}
|
||||
@@ -2393,6 +2649,8 @@ def form_builder_page(request):
|
||||
'inherit_phone_number_choice',
|
||||
]:
|
||||
conditional_field_choices.append((field_name, labels.get(field_name, field_name)))
|
||||
for cfg in custom_field_configs:
|
||||
conditional_field_choices.append((f'custom__{cfg.field_key}', cfg.translated_label(language_code)))
|
||||
conditional_target_titles = {
|
||||
'business-card-box': _('Visitenkarten-Details'),
|
||||
'employment-end-box': _('Vertragsende'),
|
||||
@@ -2417,16 +2675,26 @@ def form_builder_page(request):
|
||||
clauses = list(cfg.clauses or [])
|
||||
while len(clauses) < 2:
|
||||
clauses.append({'field': '', 'operator': 'equals', 'value': ''})
|
||||
if target_key.startswith('custom__'):
|
||||
custom_field_key = target_key.replace('custom__', '', 1)
|
||||
custom_field = next((item for item in custom_field_configs if item.field_key == custom_field_key), None)
|
||||
target_title = custom_field.translated_label(language_code) if custom_field else target_key
|
||||
target_description = _('Steuert die Sichtbarkeit dieses benutzerdefinierten Feldes.')
|
||||
target_fields = [target_title]
|
||||
else:
|
||||
target_title = conditional_target_titles.get(target_key, target_key)
|
||||
target_description = conditional_target_descriptions.get(target_key, '')
|
||||
target_fields = [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])]
|
||||
conditional_rule_items.append(
|
||||
{
|
||||
'target_key': target_key,
|
||||
'title': conditional_target_titles.get(target_key, target_key),
|
||||
'description': conditional_target_descriptions.get(target_key, ''),
|
||||
'title': target_title,
|
||||
'description': target_description,
|
||||
'is_active': cfg.is_active,
|
||||
'clauses': clauses[:2],
|
||||
'field_choices': conditional_field_choices,
|
||||
'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES,
|
||||
'target_fields': [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])],
|
||||
'target_fields': target_fields,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2441,6 +2709,16 @@ def form_builder_page(request):
|
||||
item for item in field_rule_group_map.get(key, [])
|
||||
if item['locked'] or item['is_visible']
|
||||
]
|
||||
visible_items.extend(
|
||||
[
|
||||
{
|
||||
'label': cfg.translated_label(language_code),
|
||||
'locked': False,
|
||||
}
|
||||
for cfg in custom_field_configs
|
||||
if cfg.section_key == key and cfg.is_active
|
||||
]
|
||||
)
|
||||
if section_visible:
|
||||
preview_sections.append(
|
||||
{
|
||||
@@ -2459,6 +2737,7 @@ def form_builder_page(request):
|
||||
'configurable_field_count': configurable_field_count,
|
||||
'hidden_field_count': hidden_field_count,
|
||||
'hidden_section_count': hidden_section_count,
|
||||
'custom_field_count': len([cfg for cfg in custom_field_configs if cfg.is_active]),
|
||||
}
|
||||
|
||||
return render(
|
||||
@@ -2479,6 +2758,8 @@ def form_builder_page(request):
|
||||
'section_rule_items': section_rule_items,
|
||||
'builder_summary': builder_summary,
|
||||
'conditional_rule_items': conditional_rule_items,
|
||||
'custom_field_groups': custom_field_groups,
|
||||
'custom_field_type_choices': _translate_choice_list(FormCustomFieldConfig.FIELD_TYPE_CHOICES),
|
||||
'active_panel': active_panel,
|
||||
'active_subpanel': active_subpanel,
|
||||
'available_presets': FORM_PRESETS.get(form_type, {}),
|
||||
@@ -2921,22 +3202,24 @@ def form_builder_save_order(request):
|
||||
form_type = payload.get('form_type')
|
||||
if form_type not in DEFAULT_FIELD_ORDER:
|
||||
return JsonResponse({'ok': False, 'error': 'Ungültiger Formulartyp.'}, status=400)
|
||||
default_page_map = get_default_page_map(form_type)
|
||||
|
||||
columns = payload.get('columns')
|
||||
if not isinstance(columns, dict):
|
||||
return JsonResponse({'ok': False, 'error': 'Spalten-Daten fehlen.'}, status=400)
|
||||
|
||||
configs = list(FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name'))
|
||||
allowed_names = {cfg.field_name for cfg in configs}
|
||||
custom_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_key'))
|
||||
allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs}
|
||||
seen = set()
|
||||
ordered_names = []
|
||||
|
||||
if form_type == 'onboarding':
|
||||
allowed_columns = ONBOARDING_PAGE_ORDER
|
||||
else:
|
||||
allowed_columns = ['all']
|
||||
allowed_columns = OFFBOARDING_PAGE_ORDER
|
||||
|
||||
name_to_cfg = {cfg.field_name: cfg for cfg in configs}
|
||||
custom_name_to_cfg = {f'custom__{cfg.field_key}': cfg for cfg in custom_configs}
|
||||
sort_order = 0
|
||||
|
||||
for column_key in allowed_columns:
|
||||
@@ -2950,14 +3233,15 @@ def form_builder_save_order(request):
|
||||
if name not in allowed_names or name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
ordered_names.append(name)
|
||||
cfg = name_to_cfg[name]
|
||||
cfg.sort_order = sort_order
|
||||
sort_order += 1
|
||||
if form_type == 'onboarding':
|
||||
if name in name_to_cfg:
|
||||
cfg = name_to_cfg[name]
|
||||
cfg.sort_order = sort_order
|
||||
cfg.page_key = column_key
|
||||
else:
|
||||
cfg.page_key = ''
|
||||
cfg = custom_name_to_cfg[name]
|
||||
cfg.sort_order = sort_order
|
||||
cfg.section_key = column_key
|
||||
sort_order += 1
|
||||
|
||||
missing = [cfg.field_name for cfg in configs if cfg.field_name not in seen]
|
||||
for name in missing:
|
||||
@@ -2967,11 +3251,24 @@ def form_builder_save_order(request):
|
||||
if form_type == 'onboarding':
|
||||
cfg.page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss')
|
||||
else:
|
||||
cfg.page_key = ''
|
||||
cfg.page_key = cfg.page_key or default_page_map.get(name, OFFBOARDING_PAGE_ORDER[-1])
|
||||
|
||||
missing_custom = [name for name in custom_name_to_cfg.keys() if name not in seen]
|
||||
for name in missing_custom:
|
||||
cfg = custom_name_to_cfg[name]
|
||||
cfg.sort_order = sort_order
|
||||
sort_order += 1
|
||||
if form_type == 'onboarding':
|
||||
cfg.section_key = cfg.section_key or 'abschluss'
|
||||
else:
|
||||
cfg.section_key = cfg.section_key or OFFBOARDING_PAGE_ORDER[-1]
|
||||
|
||||
FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key'])
|
||||
_audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': len(configs)})
|
||||
return JsonResponse({'ok': True, 'saved_count': len(configs)})
|
||||
if custom_configs:
|
||||
FormCustomFieldConfig.objects.bulk_update(custom_configs, ['sort_order', 'section_key'])
|
||||
saved_count = len(configs) + len(custom_configs)
|
||||
_audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': saved_count})
|
||||
return JsonResponse({'ok': True, 'saved_count': saved_count})
|
||||
|
||||
|
||||
@_require_capability('manage_integrations')
|
||||
|
||||
Reference in New Issue
Block a user