Compare commits

...

13 Commits

Author SHA1 Message Date
aminovfariz
0392043ee3 fix(helpdesk): fix user-select empty options — use nested row structure fallback
Some checks failed
deploy / QA gates (push) Has been cancelled
deploy / Build & deploy (push) Has been cancelled
DB::select with JOIN returns rows as $u['u']['field'] ?? $u['field'].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:00:25 +02:00
aminovfariz
e3dfdef76f fix(helpdesk): always load tenant users, not only when schema has user-select
Schema snapshot on existing handovers may predate the field type change,
so the conditional check was false and tenantUsers was empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:57:02 +02:00
aminovfariz
40b94fa891 fix(helpdesk): user-select as multi-select, store as JSON array
- select multiple, no placeholder option (was causing two empty entries)
- value stored as JSON array string e.g. ["1","3"]
- on render decode JSON back to array for selected check
- both create and edit actions read field as array

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:51:50 +02:00
aminovfariz
0fabb0c478 fix(helpdesk): allow user-select in schema field type validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:34:28 +02:00
aminovfariz
2968d5e386 feat(helpdesk): add user-select to schema editor field type dropdown
Adds 'user-select' to FIELD_TYPES so it appears in the schema editor
dropdown as 'Benutzerauswahl' / 'User selection'. Preview renders a
placeholder select. Translations added to both i18n files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:04:25 +02:00
aminovfariz
9bc8799b6b feat(helpdesk): add user-select field type to handover protocol schema
New schema field type 'user-select' renders a dropdown of active tenant
users. Action files (create + edit) load tenant users when the schema
contains at least one user-select field and pass them to _form.phtml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:46:15 +02:00
aminovfariz
e569e2d26c fix(helpdesk): fix manual domain value not submitted — use hidden input for domain_no
Select had name="domain_no" but JS was looking for input[name="domain_no"].
Moved name to a hidden input #wizard-domain-no; select syncs into it on
change; manual text input syncs into it on input event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:22:19 +02:00
aminovfariz
57cb2f1017 ci: trigger GitHub Actions 2026-06-08 10:05:02 +02:00
aminovfariz
0fc2d3f2c3 feat(helpdesk): show manual domain input when customer has no domains
When the domain API returns an empty list for a selected customer, the
domain select is replaced with two text inputs: domain name (required)
and URL (optional). JS syncs the manual values into the existing hidden
domain_no/domain_url fields so the server-side validation and save logic
is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:04:27 +02:00
aminovfariz
2b230e756e fix(deploy): re-resolve php upstream on each request to survive container restarts
Using resolver 127.0.0.11 (Docker internal DNS) with set $php_upstream
forces nginx to re-resolve the php hostname dynamically instead of caching
the IP at startup — eliminates 502 after php container is recreated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:58:28 +02:00
aminovfariz
b049e2490b chore(deploy): add production deploy config (docker-compose + nginx)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:37:37 +02:00
aminovfariz
16fe1b0db3 fix(ci): skip codex skills sync on hosts without Codex installation
QG-009 was failing on GitHub Actions runner because ~/.codex does not
exist on ubuntu-latest and CODEX_HOME is not set. The check is a
local-developer tool only — CI runners do not have Codex installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:29:00 +02:00
aminovfariz
322c9575f8 fix(css): wrap address-book-index.css in @layer pages block
css-contract-check.sh expects @layer pages { (block syntax),
not @layer pages; (statement syntax) — regex captures up to
the first whitespace or { so the semicolon was included in the
detected layer name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:23:09 +02:00
15 changed files with 307 additions and 13 deletions

View File

@@ -42,6 +42,13 @@ if ! command -v rsync >/dev/null 2>&1; then
exit 3 exit 3
fi fi
# Skip on CI environments where ~/.codex is not present and CODEX_HOME is not set.
# Codex skills sync is a local developer tool; CI runners do not have a Codex installation.
if [[ -z "${CODEX_HOME:-}" ]] && [[ ! -d "${HOME}/.codex" ]]; then
echo "[SKIP] Codex home not found and CODEX_HOME not set — skipping on this host."
exit 0
fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/.." && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)"
source_dir="${repo_root}/.agents/skills" source_dir="${repo_root}/.agents/skills"

72
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,72 @@
services:
# ── Init: копирует web/ из образа в shared volume (нужен nginx) ─────────
web_init:
image: breadcrumb-the-shire:latest
volumes:
- web_assets:/target
command: sh -c "cp -rp /var/www/web/. /target/ && echo 'Web assets OK'"
restart: "no"
# ── nginx: отдаёт статику + FastCGI → php ───────────────────────────────
nginx:
image: nginx:1.25-alpine
ports:
- "127.0.0.1:8084:80" # host nginx проксирует сюда
volumes:
- ./nginx/site.conf:/etc/nginx/conf.d/default.conf:ro
- web_assets:/var/www/web:ro
depends_on:
web_init:
condition: service_completed_successfully
php:
condition: service_started
restart: unless-stopped
# ── PHP-FPM: весь код внутри образа ─────────────────────────────────────
php:
image: breadcrumb-the-shire:latest
env_file: .env
volumes:
- ./storage:/var/www/storage
- ./var:/var/www/var
depends_on:
- db
- memcached
restart: unless-stopped
# ── Планировщик задач ────────────────────────────────────────────────────
scheduler:
image: breadcrumb-the-shire:latest
env_file: .env
volumes:
- ./storage:/var/www/storage
- ./var:/var/www/var
depends_on:
- db
- memcached
command: sh -c "while true; do php -d display_errors=0 bin/console scheduler:run || true; sleep 60; done"
restart: unless-stopped
# ── MariaDB ──────────────────────────────────────────────────────────────
db:
image: mariadb:11.4
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
env_file: .env
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASS}
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
# ── Memcached ────────────────────────────────────────────────────────────
memcached:
image: memcached:1.6-alpine
restart: unless-stopped
volumes:
web_assets: # статика из образа (web/)
db_data: # данные MariaDB

41
deploy/nginx/site.conf Normal file
View File

@@ -0,0 +1,41 @@
# Docker nginx — только HTTP (SSL терминируется на host nginx)
server {
listen 80;
server_name projects.breadcrumb-online.de;
client_max_body_size 10M;
root /var/www/web;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Docker DNS resolver — re-resolves php hostname on each request
resolver 127.0.0.11 valid=5s;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# SSL терминирован на host nginx — передаём это в PHP
fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO https;
set $php_upstream php:9000;
fastcgi_pass $php_upstream;
fastcgi_hide_header Cache-Control;
add_header Cache-Control "no-store, no-cache, must-revalidate, private" always;
}
location ~* \.mjs$ {
default_type application/javascript;
try_files $uri =404;
expires 30d;
access_log off;
}
location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ {
expires 30d;
access_log off;
}
}

36
deploy/setup.sh Normal file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# setup.sh — первоначальная настройка на сервере
# Запускать один раз: bash setup.sh
set -e
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DEPLOY_DIR"
echo "=== [1/4] Загружаем образ в Docker ==="
docker load < breadcrumb-the-shire.tar.gz
echo "=== [2/4] Создаём папки для данных ==="
mkdir -p storage/logs storage/runtime storage/users
mkdir -p var/cache var/log var/tmp
mkdir -p nginx/certs
echo "=== [3/4] Права на папки ==="
chmod 755 storage var
chmod 775 storage/logs storage/runtime storage/users
chmod 775 var/cache var/log var/tmp
echo "=== [4/4] Готово! ==="
echo ""
echo "Следующие шаги:"
echo " 1. Получи SSL-сертификат:"
echo " certbot certonly --standalone -d projects.breadcrumb-online.de"
echo ""
echo " 2. Скопируй сертификаты:"
echo " cp /etc/letsencrypt/live/projects.breadcrumb-online.de/fullchain.pem nginx/certs/"
echo " cp /etc/letsencrypt/live/projects.breadcrumb-online.de/privkey.pem nginx/certs/"
echo ""
echo " 3. Запусти стек:"
echo " docker compose up -d"
echo ""
echo " 4. Проверь логи:"
echo " docker compose logs -f"

View File

@@ -1,4 +1,4 @@
@layer pages; @layer pages {
/* Address book list — dense identity layout */ /* Address book list — dense identity layout */
@@ -143,3 +143,5 @@
max-width: 100%; max-width: 100%;
} }
} }
} /* end @layer pages */

View File

@@ -403,6 +403,7 @@
"Date": "Datum", "Date": "Datum",
"Checkbox": "Checkbox", "Checkbox": "Checkbox",
"Select": "Auswahl", "Select": "Auswahl",
"User selection": "Benutzerauswahl",
"Text content": "Textinhalt", "Text content": "Textinhalt",
"Maximum %d fields allowed": "Maximal %d Felder erlaubt", "Maximum %d fields allowed": "Maximal %d Felder erlaubt",
"Field %d: unknown type \"%s\"": "Feld %d: unbekannter Typ \"%s\"", "Field %d: unknown type \"%s\"": "Feld %d: unbekannter Typ \"%s\"",
@@ -488,6 +489,9 @@
"Select a customer first...": "Bitte zuerst einen Kunden auswählen...", "Select a customer first...": "Bitte zuerst einen Kunden auswählen...",
"Loading domains...": "Domains werden geladen...", "Loading domains...": "Domains werden geladen...",
"No domains found for this customer": "Keine Domains für diesen Kunden gefunden", "No domains found for this customer": "Keine Domains für diesen Kunden gefunden",
"No domains found for this customer. Enter domain name manually:": "Keine Domains für diesen Kunden gefunden. Domain-Name manuell eingeben:",
"Domain name (e.g. example.de)": "Domain-Name (z. B. example.de)",
"URL (optional, e.g. https://example.de)": "URL (optional, z. B. https://example.de)",
"Failed to load domains": "Domains konnten nicht geladen werden", "Failed to load domains": "Domains konnten nicht geladen werden",
"Please select a domain": "Bitte wählen Sie eine Domain", "Please select a domain": "Bitte wählen Sie eine Domain",
"Delete": "Löschen", "Delete": "Löschen",

View File

@@ -403,6 +403,7 @@
"Date": "Date", "Date": "Date",
"Checkbox": "Checkbox", "Checkbox": "Checkbox",
"Select": "Select", "Select": "Select",
"User selection": "User selection",
"Text content": "Text content", "Text content": "Text content",
"Maximum %d fields allowed": "Maximum %d fields allowed", "Maximum %d fields allowed": "Maximum %d fields allowed",
"Field %d: unknown type \"%s\"": "Field %d: unknown type \"%s\"", "Field %d: unknown type \"%s\"": "Field %d: unknown type \"%s\"",
@@ -488,6 +489,9 @@
"Select a customer first...": "Select a customer first...", "Select a customer first...": "Select a customer first...",
"Loading domains...": "Loading domains...", "Loading domains...": "Loading domains...",
"No domains found for this customer": "No domains found for this customer", "No domains found for this customer": "No domains found for this customer",
"No domains found for this customer. Enter domain name manually:": "No domains found for this customer. Enter domain name manually:",
"Domain name (e.g. example.de)": "Domain name (e.g. example.de)",
"URL (optional, e.g. https://example.de)": "URL (optional, e.g. https://example.de)",
"Failed to load domains": "Failed to load domains", "Failed to load domains": "Failed to load domains",
"Please select a domain": "Please select a domain", "Please select a domain": "Please select a domain",
"Delete": "Delete", "Delete": "Delete",

View File

@@ -38,7 +38,7 @@ class SoftwareProductService
return $this->repository->listWithSchema(); return $this->repository->listWithSchema();
} }
private const ALLOWED_FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select']; private const ALLOWED_FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select', 'user-select'];
private const DISPLAY_ONLY_TYPES = ['heading', 'paragraph']; private const DISPLAY_ONLY_TYPES = ['heading', 'paragraph'];
private const MAX_SCHEMA_FIELDS = 50; private const MAX_SCHEMA_FIELDS = 50;

View File

@@ -11,6 +11,7 @@
$schemaFields = is_array($schemaFields ?? null) ? $schemaFields : []; $schemaFields = is_array($schemaFields ?? null) ? $schemaFields : [];
$fieldValues = is_array($fieldValues ?? null) ? $fieldValues : []; $fieldValues = is_array($fieldValues ?? null) ? $fieldValues : [];
$fieldDiff = is_array($fieldDiff ?? null) ? $fieldDiff : []; $fieldDiff = is_array($fieldDiff ?? null) ? $fieldDiff : [];
$tenantUsers = is_array($tenantUsers ?? null) ? $tenantUsers : [];
$readonly = !empty($readonly); $readonly = !empty($readonly);
$hasDiff = $fieldDiff !== []; $hasDiff = $fieldDiff !== [];
?> ?>
@@ -56,6 +57,43 @@ $hasDiff = $fieldDiff !== [];
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php elseif ($type === 'user-select'): ?>
<?php
$selectedIds = [];
if ($value !== '') {
$decoded = json_decode($value, true);
$selectedIds = is_array($decoded) ? array_map('strval', $decoded) : [$value];
}
?>
<div class="handover-diff-field<?php e($diffClass); ?>">
<label for="<?php e($inputId); ?>">
<?php e($label); ?>
<?php if ($required): ?><span class="handover-form-required"> *</span><?php endif; ?>
</label>
<select
id="<?php e($inputId); ?>"
name="field_<?php e($key); ?>[]"
multiple
size="5"
<?php if ($required): ?> aria-required="true"<?php endif; ?>
<?php if ($readonly): ?> disabled<?php endif; ?>
>
<?php foreach ($tenantUsers as $u):
$uId = (string) ($u['u']['id'] ?? $u['id'] ?? '');
$uName = trim((string) ($u['u']['display_name'] ?? $u['display_name'] ?? ''));
$uEmail = (string) ($u['u']['email'] ?? $u['email'] ?? '');
$uLabel = $uName !== '' ? $uName : $uEmail;
?>
<option value="<?php e($uId); ?>"<?php if (in_array($uId, $selectedIds, true)): ?> selected<?php endif; ?>>
<?php e($uLabel); ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($diff !== null && $diff['type'] === 'changed'): ?>
<small class="app-muted handover-diff-indicator" aria-label="<?php e(t('Changed')); ?>"><?php e(t('was: %s', (string) ($diff['old'] ?? ''))); ?></small>
<?php endif; ?>
</div>
<?php elseif ($type === 'select'): ?> <?php elseif ($type === 'select'): ?>
<div class="handover-diff-field<?php e($diffClass); ?>"> <div class="handover-diff-field<?php e($diffClass); ?>">
<label for="<?php e($inputId); ?>"> <label for="<?php e($inputId); ?>">

View File

@@ -230,6 +230,17 @@ if ($step === $protocolStep) {
? $product['name'] ? $product['name']
: (string) ($product['bc_description'] ?? $wizardData['product_code']); : (string) ($product['bc_description'] ?? $wizardData['product_code']);
// Load tenant users — always available for user-select schema fields
$rows = DB::select(
'SELECT u.id, u.display_name, u.email'
. ' FROM users u'
. ' JOIN user_tenants ut ON ut.user_id = u.id'
. ' WHERE ut.tenant_id = ? AND u.active = 1'
. ' ORDER BY u.display_name ASC',
(string) $currentTenantId
);
$tenantUsers = is_array($rows) ? $rows : [];
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
if (!Session::checkCsrfToken()) { if (!Session::checkCsrfToken()) {
$modeParam = $canManage ? '&mode=' . ($wizardData['mode'] ?? 'self') : ''; $modeParam = $canManage ? '&mode=' . ($wizardData['mode'] ?? 'self') : '';
@@ -246,6 +257,9 @@ if ($step === $protocolStep) {
$type = $field['type'] ?? 'text'; $type = $field['type'] ?? 'text';
if ($type === 'checkbox') { if ($type === 'checkbox') {
$fieldValues[$key] = $request->body('field_' . $key, '') !== '' ? '1' : '0'; $fieldValues[$key] = $request->body('field_' . $key, '') !== '' ? '1' : '0';
} elseif ($type === 'user-select') {
$ids = $request->body('field_' . $key, []);
$fieldValues[$key] = json_encode(array_values(array_filter((array) $ids)));
} else { } else {
$fieldValues[$key] = (string) $request->body('field_' . $key, ''); $fieldValues[$key] = (string) $request->body('field_' . $key, '');
} }

View File

@@ -149,11 +149,24 @@ if ($canManage && $mode === 'assign' && $step === 2) {
data-initial-domain="<?php e($form['domain_no'] ?? ''); ?>" data-initial-domain="<?php e($form['domain_no'] ?? ''); ?>"
> >
<label for="wizard-domain"><?php e(t('Domain')); ?> <span class="handover-form-required">*</span></label> <label for="wizard-domain"><?php e(t('Domain')); ?> <span class="handover-form-required">*</span></label>
<select id="wizard-domain" name="domain_no" disabled> <div class="app-wizard-domain-select-wrap">
<option value=""><?php e(t('Select a customer first...')); ?></option> <select id="wizard-domain" disabled>
</select> <option value=""><?php e(t('Select a customer first...')); ?></option>
</select>
</div>
<input type="hidden" name="domain_no" id="wizard-domain-no" value="<?php e($form['domain_no'] ?? ''); ?>">
<input type="hidden" name="domain_url" id="wizard-domain-url" value="<?php e($form['domain_url'] ?? ''); ?>"> <input type="hidden" name="domain_url" id="wizard-domain-url" value="<?php e($form['domain_url'] ?? ''); ?>">
<div id="wizard-domain-feedback" class="app-wizard-field-feedback" role="alert" hidden></div> <div id="wizard-domain-feedback" class="app-wizard-field-feedback" role="alert" hidden></div>
<!-- Manual entry — shown by JS when no domains exist for this customer -->
<div id="wizard-domain-manual" hidden>
<p class="app-wizard-field-hint"><?php e(t('No domains found for this customer. Enter domain name manually:')); ?></p>
<input type="text" id="wizard-domain-manual-no" placeholder="<?php e(t('Domain name (e.g. example.de)')); ?>"
value="<?php e($form['domain_no'] ?? ''); ?>" autocomplete="off">
<input type="text" id="wizard-domain-manual-url" placeholder="<?php e(t('URL (optional, e.g. https://example.de)')); ?>"
value="<?php e($form['domain_url'] ?? ''); ?>" autocomplete="off" style="margin-top:0.4rem;">
</div>
<?php if (!empty($errors['domain_no'])): ?> <?php if (!empty($errors['domain_no'])): ?>
<p class="field-error"><?php e($errors['domain_no']); ?></p> <p class="field-error"><?php e($errors['domain_no']); ?></p>
<?php endif; ?> <?php endif; ?>

View File

@@ -1,6 +1,7 @@
<?php <?php
use MintyPHP\Buffer; use MintyPHP\Buffer;
use MintyPHP\DB;
use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy; use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService; use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
@@ -147,6 +148,9 @@ if ($request->isMethod('POST') && $canEdit) {
$type = $field['type'] ?? 'text'; $type = $field['type'] ?? 'text';
if ($type === 'checkbox') { if ($type === 'checkbox') {
$submittedValues[$key] = $request->body('field_' . $key, '') !== '' ? '1' : '0'; $submittedValues[$key] = $request->body('field_' . $key, '') !== '' ? '1' : '0';
} elseif ($type === 'user-select') {
$ids = $request->body('field_' . $key, []);
$submittedValues[$key] = json_encode(array_values(array_filter((array) $ids)));
} else { } else {
$submittedValues[$key] = (string) $request->body('field_' . $key, ''); $submittedValues[$key] = (string) $request->body('field_' . $key, '');
} }
@@ -201,6 +205,17 @@ if ($request->isMethod('POST') && $canEdit) {
$validationSummaryErrors = $errorBag->toArray(); $validationSummaryErrors = $errorBag->toArray();
$errors = $errorBag->toFlatList(); $errors = $errorBag->toFlatList();
// Load tenant users — always available for user-select schema fields
$rows = DB::select(
'SELECT u.id, u.display_name, u.email'
. ' FROM users u'
. ' JOIN user_tenants ut ON ut.user_id = u.id'
. ' WHERE ut.tenant_id = ? AND u.active = 1'
. ' ORDER BY u.display_name ASC',
(string) $tenantId
);
$tenantUsers = is_array($rows) ? $rows : [];
// Load revision data // Load revision data
$revisions = $revisionService->listByHandover($tenantId, $handoverId); $revisions = $revisionService->listByHandover($tenantId, $handoverId);
$activeRevisionNumber = (int) $request->query('revision', 0); $activeRevisionNumber = (int) $request->query('revision', 0);

View File

@@ -53,6 +53,7 @@ $handoverSchemaFields = is_array($handoverSchemaFields ?? null) ? $handoverSchem
'type_date' => t('Date'), 'type_date' => t('Date'),
'type_checkbox' => t('Checkbox'), 'type_checkbox' => t('Checkbox'),
'type_select' => t('Select'), 'type_select' => t('Select'),
'type_user-select' => t('User selection'),
'text_content' => t('Text content'), 'text_content' => t('Text content'),
'field' => t('Field'), 'field' => t('Field'),
'preview' => t('Preview'), 'preview' => t('Preview'),

View File

@@ -23,8 +23,12 @@ export function initHandoverDomainSelect(root = document) {
const scope = group.closest('form') || document; const scope = group.closest('form') || document;
const lookupContainer = scope.querySelector('[data-app-lookup]'); const lookupContainer = scope.querySelector('[data-app-lookup]');
const domainSelect = group.querySelector('#wizard-domain'); const domainSelect = group.querySelector('#wizard-domain');
const domainNoInput = group.querySelector('#wizard-domain-no');
const domainUrlInput = group.querySelector('#wizard-domain-url'); const domainUrlInput = group.querySelector('#wizard-domain-url');
const feedback = group.querySelector('#wizard-domain-feedback'); const feedback = group.querySelector('#wizard-domain-feedback');
const manualBlock = group.querySelector('#wizard-domain-manual');
const manualNoInput = group.querySelector('#wizard-domain-manual-no');
const manualUrlInput = group.querySelector('#wizard-domain-manual-url');
if (!lookupContainer || !domainSelect) { if (!lookupContainer || !domainSelect) {
return EMPTY_API; return EMPTY_API;
} }
@@ -67,14 +71,33 @@ export function initHandoverDomainSelect(root = document) {
feedback.dataset.variant = variant || ''; feedback.dataset.variant = variant || '';
}; };
const showManual = (show) => {
if (!manualBlock) {
return;
}
manualBlock.hidden = !show;
domainSelect.closest('.app-wizard-domain-select-wrap')?.toggleAttribute('hidden', show);
if (!show && manualNoInput) {
manualNoInput.value = '';
}
if (!show && manualUrlInput) {
manualUrlInput.value = '';
}
// When showing manual, clear the hidden domain_no/url so only manual values submit
if (show) {
if (domainNoInput) domainNoInput.value = '';
if (domainUrlInput) domainUrlInput.value = '';
}
};
const resetDomain = () => { const resetDomain = () => {
clearOptions(); clearOptions();
addPlaceholder(textInitial); addPlaceholder(textInitial);
domainSelect.disabled = true; domainSelect.disabled = true;
if (domainUrlInput) { if (domainNoInput) domainNoInput.value = '';
domainUrlInput.value = ''; if (domainUrlInput) domainUrlInput.value = '';
}
setFeedback('', ''); setFeedback('', '');
showManual(false);
}; };
const loadDomains = async (debitorNo) => { const loadDomains = async (debitorNo) => {
@@ -95,9 +118,11 @@ export function initHandoverDomainSelect(root = document) {
if (items.length === 0) { if (items.length === 0) {
domainSelect.disabled = true; domainSelect.disabled = true;
setFeedback(textEmpty, 'warning'); setFeedback('', '');
showManual(true);
return; return;
} }
showManual(false);
items.forEach((item) => { items.forEach((item) => {
const opt = document.createElement('option'); const opt = document.createElement('option');
@@ -121,11 +146,24 @@ export function initHandoverDomainSelect(root = document) {
on(domainSelect, 'change', () => { on(domainSelect, 'change', () => {
const selected = domainSelect.options[domainSelect.selectedIndex]; const selected = domainSelect.options[domainSelect.selectedIndex];
if (domainUrlInput) { if (domainNoInput) domainNoInput.value = selected?.value || '';
domainUrlInput.value = selected?.dataset.url || ''; if (domainUrlInput) domainUrlInput.value = selected?.dataset.url || '';
}
}); });
// Sync manual inputs → hidden domain_no / domain_url fields
if (manualNoInput) {
on(manualNoInput, 'input', () => {
if (domainNoInput) domainNoInput.value = manualNoInput.value.trim();
});
}
if (manualUrlInput) {
on(manualUrlInput, 'input', () => {
if (domainUrlInput) {
domainUrlInput.value = manualUrlInput.value.trim();
}
});
}
on(lookupContainer, 'lookup:select', (event) => { on(lookupContainer, 'lookup:select', (event) => {
const debitorNo = event.detail?.value || ''; const debitorNo = event.detail?.value || '';
if (debitorNo) { if (debitorNo) {
@@ -148,6 +186,9 @@ export function initHandoverDomainSelect(root = document) {
domainSelect.dispatchEvent(new Event('change')); domainSelect.dispatchEvent(new Event('change'));
} }
}); });
} else if (domainNoInput && domainNoInput.value) {
// Restore hidden value on page reload with validation errors (manual mode)
if (manualNoInput) manualNoInput.value = domainNoInput.value;
} }
return { return {

View File

@@ -6,7 +6,7 @@
*/ */
import { resolveHost } from '/js/core/app-dom.js'; import { resolveHost } from '/js/core/app-dom.js';
const FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select']; const FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select', 'user-select'];
const DISPLAY_ONLY_TYPES = ['heading', 'paragraph']; const DISPLAY_ONLY_TYPES = ['heading', 'paragraph'];
const EMPTY_API = Object.freeze({ destroy: () => {} }); const EMPTY_API = Object.freeze({ destroy: () => {} });
@@ -385,6 +385,12 @@ function init(root) {
option.textContent = opt.label || ''; option.textContent = opt.label || '';
input.appendChild(option); input.appendChild(option);
}); });
} else if (field.type === 'user-select') {
input = document.createElement('select');
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t['type_user-select'] || 'User selection';
input.appendChild(placeholder);
} else { } else {
input = document.createElement('input'); input = document.createElement('input');
input.type = field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'; input.type = field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text';