add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Service\CustomField;
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepository;
|
|
|
|
|
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository;
|
|
|
|
|
use MintyPHP\Repository\Tenant\TenantRepository;
|
|
|
|
|
|
|
|
|
|
class TenantCustomFieldService
|
|
|
|
|
{
|
|
|
|
|
private const ALLOWED_TYPES = ['text', 'textarea', 'select', 'multiselect', 'boolean', 'date'];
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
public function __construct(
|
|
|
|
|
private readonly TenantRepository $tenantRepository
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function listForTenant(int $tenantId): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$tenantId = (int) $tenantId;
|
|
|
|
|
if ($tenantId <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$definitions = TenantCustomFieldDefinitionRepository::listByTenantId($tenantId, false);
|
|
|
|
|
if (!$definitions) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$definitionIds = array_map(static fn (array $row): int => (int) ($row['id'] ?? 0), $definitions);
|
|
|
|
|
$definitionIds = array_values(array_filter($definitionIds, static fn (int $id): bool => $id > 0));
|
|
|
|
|
$options = TenantCustomFieldOptionRepository::listByDefinitionIds($definitionIds, false);
|
|
|
|
|
$optionsByDefinition = [];
|
|
|
|
|
foreach ($options as $option) {
|
|
|
|
|
$definitionId = (int) ($option['definition_id'] ?? 0);
|
|
|
|
|
if ($definitionId <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$optionsByDefinition[$definitionId] ??= [];
|
|
|
|
|
$optionsByDefinition[$definitionId][] = $option;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($definitions as &$definition) {
|
|
|
|
|
$definitionId = (int) ($definition['id'] ?? 0);
|
|
|
|
|
$definition['options'] = $optionsByDefinition[$definitionId] ?? [];
|
|
|
|
|
}
|
|
|
|
|
unset($definition);
|
|
|
|
|
|
|
|
|
|
return $definitions;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
public function createForTenant(int $tenantId, array $input, int $currentUserId): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
if ($tenantId <= 0 || !$this->tenantRepository->find($tenantId)) {
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
return ['ok' => false, 'errors' => [t('Tenant not found')]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$form = self::sanitizeForm($input);
|
|
|
|
|
$form['tenant_id'] = $tenantId;
|
|
|
|
|
$form['field_key'] = self::resolveFieldKey(
|
|
|
|
|
$tenantId,
|
|
|
|
|
(string) ($form['label'] ?? ''),
|
|
|
|
|
null,
|
|
|
|
|
(string) ($form['field_key'] ?? '')
|
|
|
|
|
);
|
|
|
|
|
$errors = self::validateForm($form, null);
|
|
|
|
|
if ($errors) {
|
|
|
|
|
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$definitionId = TenantCustomFieldDefinitionRepository::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'field_key' => $form['field_key'],
|
|
|
|
|
'label' => $form['label'],
|
|
|
|
|
'type' => $form['type'],
|
|
|
|
|
'is_required' => $form['is_required'],
|
|
|
|
|
'is_filterable' => $form['is_filterable'],
|
|
|
|
|
'active' => $form['active'],
|
|
|
|
|
'sort_order' => $form['sort_order'] ?? self::nextSortOrder($tenantId),
|
|
|
|
|
'created_by' => $currentUserId > 0 ? $currentUserId : null,
|
|
|
|
|
]);
|
|
|
|
|
if (!$definitionId) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Custom field can not be created')], 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (in_array($form['type'], ['select', 'multiselect'], true)) {
|
|
|
|
|
TenantCustomFieldOptionRepository::replaceForDefinition((int) $definitionId, $form['options']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['ok' => true, 'id' => (int) $definitionId, 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
public function updateByUuid(string $uuid, array $input, int $currentUserId): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$uuid = trim($uuid);
|
|
|
|
|
if ($uuid === '') {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Custom field not found')]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$existing = TenantCustomFieldDefinitionRepository::findByUuid($uuid);
|
|
|
|
|
if (!$existing) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Custom field not found')]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$form = self::sanitizeForm($input);
|
|
|
|
|
$form['tenant_id'] = (int) ($existing['tenant_id'] ?? 0);
|
|
|
|
|
$form['field_key'] = self::resolveFieldKey(
|
|
|
|
|
(int) ($existing['tenant_id'] ?? 0),
|
|
|
|
|
(string) ($form['label'] ?? ''),
|
|
|
|
|
(int) ($existing['id'] ?? 0),
|
|
|
|
|
(string) ($existing['field_key'] ?? '')
|
|
|
|
|
);
|
|
|
|
|
$errors = self::validateForm($form, (int) ($existing['id'] ?? 0));
|
|
|
|
|
if ($errors) {
|
|
|
|
|
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$updated = TenantCustomFieldDefinitionRepository::update((int) $existing['id'], [
|
|
|
|
|
'field_key' => $form['field_key'],
|
|
|
|
|
'label' => $form['label'],
|
|
|
|
|
'type' => $form['type'],
|
|
|
|
|
'is_required' => $form['is_required'],
|
|
|
|
|
'is_filterable' => $form['is_filterable'],
|
|
|
|
|
'active' => $form['active'],
|
|
|
|
|
'sort_order' => $form['sort_order'] ?? (int) ($existing['sort_order'] ?? 100),
|
|
|
|
|
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
|
|
|
|
|
]);
|
|
|
|
|
if (!$updated) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Custom field can not be updated')], 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// When type is changed away from select/multiselect, remove orphaned option rows.
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (in_array($form['type'], ['select', 'multiselect'], true)) {
|
|
|
|
|
TenantCustomFieldOptionRepository::replaceForDefinition((int) $existing['id'], $form['options']);
|
|
|
|
|
} else {
|
|
|
|
|
TenantCustomFieldOptionRepository::deleteByDefinitionId((int) $existing['id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['ok' => true, 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
public function deleteByUuid(string $uuid): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$uuid = trim($uuid);
|
|
|
|
|
if ($uuid === '') {
|
|
|
|
|
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
|
|
|
|
}
|
|
|
|
|
$existing = TenantCustomFieldDefinitionRepository::findByUuid($uuid);
|
|
|
|
|
if (!$existing || !isset($existing['id'])) {
|
|
|
|
|
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
|
|
|
|
}
|
|
|
|
|
$deleted = TenantCustomFieldDefinitionRepository::delete((int) $existing['id']);
|
|
|
|
|
if (!$deleted) {
|
|
|
|
|
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
|
|
|
|
|
}
|
|
|
|
|
return ['ok' => true, 'definition' => $existing];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
public function definitionTenantIdByUuid(string $uuid): int
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$definition = TenantCustomFieldDefinitionRepository::findByUuid(trim($uuid));
|
|
|
|
|
return (int) ($definition['tenant_id'] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function sanitizeForm(array $input): array
|
|
|
|
|
{
|
|
|
|
|
$label = trim((string) ($input['label'] ?? ''));
|
|
|
|
|
$fieldKeyInput = trim((string) ($input['field_key'] ?? ''));
|
|
|
|
|
$type = strtolower(trim((string) ($input['type'] ?? 'text')));
|
|
|
|
|
$sortOrderRaw = $input['sort_order'] ?? null;
|
|
|
|
|
$sortOrder = is_numeric($sortOrderRaw) ? (int) $sortOrderRaw : null;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'label' => $label,
|
|
|
|
|
'field_key' => self::normalizeKey($fieldKeyInput),
|
|
|
|
|
'type' => $type,
|
|
|
|
|
// User custom fields are always optional in V1.
|
|
|
|
|
'is_required' => 0,
|
|
|
|
|
'is_filterable' => !empty($input['is_filterable']) ? 1 : 0,
|
|
|
|
|
'active' => array_key_exists('active', $input) ? (int) (!empty($input['active'])) : 1,
|
|
|
|
|
'sort_order' => $sortOrder !== null && $sortOrder > 0 ? $sortOrder : null,
|
|
|
|
|
'options' => self::parseOptionsLines((string) ($input['options_text'] ?? '')),
|
|
|
|
|
'options_text' => (string) ($input['options_text'] ?? ''),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function validateForm(array &$form, ?int $excludeId): array
|
|
|
|
|
{
|
|
|
|
|
$errors = [];
|
|
|
|
|
$tenantId = (int) ($form['tenant_id'] ?? 0);
|
|
|
|
|
$type = (string) ($form['type'] ?? '');
|
|
|
|
|
$fieldKey = (string) ($form['field_key'] ?? '');
|
|
|
|
|
|
|
|
|
|
if ($tenantId <= 0) {
|
|
|
|
|
$errors[] = t('Tenant not found');
|
|
|
|
|
}
|
|
|
|
|
if ($form['label'] === '') {
|
|
|
|
|
$errors[] = t('Label cannot be empty');
|
|
|
|
|
}
|
|
|
|
|
if ($fieldKey === '') {
|
|
|
|
|
$errors[] = t('Field key cannot be empty');
|
|
|
|
|
}
|
|
|
|
|
if (!in_array($type, self::ALLOWED_TYPES, true)) {
|
|
|
|
|
$errors[] = t('Field type is invalid');
|
|
|
|
|
}
|
|
|
|
|
if ($fieldKey !== '') {
|
|
|
|
|
$existingByKey = TenantCustomFieldDefinitionRepository::findByTenantIdAndKey($tenantId, $fieldKey);
|
|
|
|
|
if ($existingByKey && (int) ($existingByKey['id'] ?? 0) !== (int) ($excludeId ?? 0)) {
|
|
|
|
|
$errors[] = t('Field key already exists');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (in_array($type, ['select', 'multiselect'], true)) {
|
|
|
|
|
if (!$form['options']) {
|
|
|
|
|
$errors[] = t('Options are required for this field type');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$form['is_filterable'] = in_array($type, ['boolean', 'date'], true)
|
|
|
|
|
? (int) ($form['is_filterable'] ?? 0)
|
|
|
|
|
: 0;
|
|
|
|
|
$form['options'] = [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// Text/textarea types can't be used as filters — too many distinct values to enumerate in UI.
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!in_array($type, ['select', 'multiselect', 'boolean', 'date'], true)) {
|
|
|
|
|
$form['is_filterable'] = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// Line format: "key|Label" or just "Label" (key is derived from the label when omitted).
|
|
|
|
|
// Duplicate keys are silently dropped — first occurrence wins.
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
private static function parseOptionsLines(string $value): array
|
|
|
|
|
{
|
|
|
|
|
$lines = preg_split('/\r\n|\r|\n/', $value);
|
|
|
|
|
if (!is_array($lines)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
$options = [];
|
|
|
|
|
$sortOrder = 10;
|
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
|
$line = trim((string) $line);
|
|
|
|
|
if ($line === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$parts = explode('|', $line, 2);
|
|
|
|
|
$label = trim((string) ($parts[1] ?? $parts[0]));
|
|
|
|
|
$keyInput = trim((string) ($parts[1] ?? '') !== '' ? $parts[0] : $label);
|
|
|
|
|
$optionKey = self::normalizeKey($keyInput);
|
|
|
|
|
if ($label === '' || $optionKey === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (isset($options[$optionKey])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$options[$optionKey] = [
|
|
|
|
|
'option_key' => $optionKey,
|
|
|
|
|
'label' => $label,
|
|
|
|
|
'active' => 1,
|
|
|
|
|
'sort_order' => $sortOrder,
|
|
|
|
|
];
|
|
|
|
|
$sortOrder += 10;
|
|
|
|
|
}
|
|
|
|
|
return array_values($options);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// Transliterates accented characters (é → e, ü → u) before slugifying so non-ASCII labels
|
|
|
|
|
// produce readable keys. Falls back to direct slugification if iconv is unavailable.
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
private static function normalizeKey(string $value): string
|
|
|
|
|
{
|
|
|
|
|
$value = trim($value);
|
|
|
|
|
if ($value === '') {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
if (function_exists('iconv')) {
|
|
|
|
|
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
|
|
|
|
if (is_string($ascii) && $ascii !== '') {
|
|
|
|
|
$value = $ascii;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$value = strtolower($value);
|
|
|
|
|
$value = preg_replace('/[^a-z0-9]+/', '_', $value);
|
|
|
|
|
$value = trim((string) $value, '_');
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function nextSortOrder(int $tenantId): int
|
|
|
|
|
{
|
|
|
|
|
$max = 0;
|
|
|
|
|
foreach (TenantCustomFieldDefinitionRepository::listByTenantId($tenantId, false) as $definition) {
|
|
|
|
|
$sortOrder = (int) ($definition['sort_order'] ?? 0);
|
|
|
|
|
if ($sortOrder > $max) {
|
|
|
|
|
$max = $sortOrder;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ($max <= 0) {
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
return $max + 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function resolveFieldKey(int $tenantId, string $label, ?int $excludeId, string $preferred): string
|
|
|
|
|
{
|
|
|
|
|
$excludeId = (int) ($excludeId ?? 0);
|
|
|
|
|
$source = trim($preferred) !== '' ? trim($preferred) : trim($label);
|
|
|
|
|
$base = self::normalizeKey($source);
|
|
|
|
|
if ($base === '') {
|
|
|
|
|
$fallbackSeed = trim($label) !== '' ? trim($label) : $source;
|
|
|
|
|
$base = 'field_' . substr(sha1($fallbackSeed), 0, 8);
|
|
|
|
|
}
|
|
|
|
|
if ($tenantId <= 0) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// Auto-increment suffix until a unique key is found; 10000 guard prevents infinite loop.
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$candidate = $base;
|
|
|
|
|
$counter = 2;
|
|
|
|
|
while (true) {
|
|
|
|
|
$existing = TenantCustomFieldDefinitionRepository::findByTenantIdAndKey($tenantId, $candidate);
|
|
|
|
|
if (!$existing || (int) ($existing['id'] ?? 0) === $excludeId) {
|
|
|
|
|
return $candidate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$candidate = $base . '_' . $counter;
|
|
|
|
|
$counter++;
|
|
|
|
|
if ($counter > 10000) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|