Compare commits
13 Commits
3ff302cc02
...
0392043ee3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0392043ee3 | ||
|
|
e3dfdef76f | ||
|
|
40b94fa891 | ||
|
|
0fabb0c478 | ||
|
|
2968d5e386 | ||
|
|
9bc8799b6b | ||
|
|
e569e2d26c | ||
|
|
57cb2f1017 | ||
|
|
0fc2d3f2c3 | ||
|
|
2b230e756e | ||
|
|
b049e2490b | ||
|
|
16fe1b0db3 | ||
|
|
322c9575f8 |
@@ -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
72
deploy/docker-compose.yml
Normal 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
41
deploy/nginx/site.conf
Normal 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
36
deploy/setup.sh
Normal 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"
|
||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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); ?>">
|
||||||
|
|||||||
@@ -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, '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; ?>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user