diff --git a/Domain/Access/AttributeFilter.php b/Domain/Access/AttributeFilter.php index 887e9a0b5..3403b5708 100644 --- a/Domain/Access/AttributeFilter.php +++ b/Domain/Access/AttributeFilter.php @@ -27,9 +27,9 @@ public static function Create($entityTableAndColumn, $attributes) $idEquals = new SqlFilterEquals($attributeId, $attribute->Id()); $f->AppendSql('LEFT JOIN `' . TableNames::CUSTOM_ATTRIBUTE_VALUES . '` `a' . $id . '` ON `a0`.`entity_id` = `a' . $id . '`.`entity_id` '); - if ($attribute->Type() == CustomAttributeTypes::MULTI_LINE_TEXTBOX || $attribute->Type() == CustomAttributeTypes::SINGLE_LINE_TEXTBOX) { + if ((int)$attribute->Type() === CustomAttributeTypes::MULTI_LINE_TEXTBOX || (int)$attribute->Type() === CustomAttributeTypes::SINGLE_LINE_TEXTBOX) { $attributeFragment->_And($idEquals->_And(new SqlFilterLike($attributeValue, $attribute->Value()))); - } elseif ($attribute->Type() == CustomAttributeTypes::CHECKBOX && $attribute->Value() == '0') { + } elseif ((int)$attribute->Type() === CustomAttributeTypes::CHECKBOX && $attribute->Value() == '0') { $attributeFragment->_And(new SqlFilterFreeForm('NOT EXISTS (SELECT 1 FROM `' . TableNames::CUSTOM_ATTRIBUTE_VALUES . '` `b` WHERE `b`.`entity_id` = `a0`.`entity_id` AND `b`.`custom_attribute_id` = ' . $id . ')')); } else { $attributeFragment->_And($idEquals->_And(new SqlFilterEquals($attributeValue, $attribute->Value()))); diff --git a/Domain/CustomAttribute.php b/Domain/CustomAttribute.php index 62d1bd97a..4c7802a51 100644 --- a/Domain/CustomAttribute.php +++ b/Domain/CustomAttribute.php @@ -308,7 +308,7 @@ public function __construct( $this->category = $category; $this->SetRegex($regex); $this->required = $required; - if ($category != CustomAttributeCategory::RESERVATION) { + if ((int)$category !== CustomAttributeCategory::RESERVATION) { $this->entityIds = is_array($entityIds) ? $entityIds : ($entityIds); } $this->adminOnly = $adminOnly; @@ -452,7 +452,7 @@ public function Update($label, $regex, $required, $possibleValues, $sortOrder, $ $this->SetRegex($regex); $this->required = $required; - if ($this->category != CustomAttributeCategory::RESERVATION) { + if ((int)$this->category !== CustomAttributeCategory::RESERVATION) { $entityIds = is_array($entityIds) ? $entityIds : [$entityIds]; $removed = array_diff($this->entityIds, $entityIds); $added = array_diff($entityIds, $this->entityIds); @@ -503,7 +503,7 @@ public function WithEntityDescriptions($entityDescriptions) */ public function WithSecondaryEntities($category, $entityIds, $entityDescriptions = null) { - if ($this->category != CustomAttributeCategory::RESERVATION) { + if ((int)$this->category !== CustomAttributeCategory::RESERVATION && (int)$this->category !== CustomAttributeCategory::RESOURCE) { return; } diff --git a/Pages/Admin/ManageAttributesPage.php b/Pages/Admin/ManageAttributesPage.php index 19d832438..fb642f08c 100644 --- a/Pages/Admin/ManageAttributesPage.php +++ b/Pages/Admin/ManageAttributesPage.php @@ -13,12 +13,12 @@ public function GetLabel(); /** * @abstract - * return int|CustomAttributeTypes + * return CustomAttributeTypes */ public function GetType(); /** - * return int|CustomAttributeCategory + * return CustomAttributeCategory */ public function GetCategory(); @@ -43,7 +43,7 @@ public function GetEntityIds(); public function GetPossibleValues(); /** - * return int|CustomAttributeCategory + * return CustomAttributeCategory */ public function GetRequestedCategory(); @@ -63,7 +63,7 @@ public function GetIsAdminOnly(); public function BindAttributes($attributes); /** - * @param $categoryId int|CustomAttributeCategory + * @param $categoryId CustomAttributeCategory */ public function SetCategory($categoryId); @@ -73,12 +73,12 @@ public function SetCategory($categoryId); public function GetAttributeId(); /** - * @return int|null + * @return int[] */ public function GetSecondaryEntityIds(); /** - * @return CustomAttributeCategory|int|null + * @return CustomAttributeCategory|null */ public function GetSecondaryCategory(); @@ -91,6 +91,11 @@ public function GetLimitAttributeScope(); * @return bool */ public function GetIsPrivate(); + + /** + * Set template variables for category-based field visibility + */ + public function SetCategoryVisibilityRules(); } class ManageAttributesPage extends ActionPage implements IManageAttributesPage @@ -240,6 +245,39 @@ public function GetIsPrivate() return !empty($isPrivate); } + public function SetCategoryVisibilityRules() + { + // Define which fields are visible for which categories + // This moves the visibility logic from JavaScript to PHP/Smarty + $this->Set('ShowAppliesToFor', [ + CustomAttributeCategory::USER => true, + CustomAttributeCategory::RESOURCE_TYPE => true, + CustomAttributeCategory::RESERVATION => false, + CustomAttributeCategory::RESOURCE => false, + ]); + + $this->Set('ShowAdminOnlyFor', [ + CustomAttributeCategory::RESERVATION => true, + CustomAttributeCategory::RESOURCE => true, + CustomAttributeCategory::USER => true, + CustomAttributeCategory::RESOURCE_TYPE => true, + ]); + + $this->Set('ShowPrivateFor', [ + CustomAttributeCategory::RESERVATION => true, + CustomAttributeCategory::RESOURCE => false, + CustomAttributeCategory::USER => false, + CustomAttributeCategory::RESOURCE_TYPE => false, + ]); + + $this->Set('ShowSecondaryEntitiesFor', [ + CustomAttributeCategory::RESERVATION => true, + CustomAttributeCategory::RESOURCE => true, + CustomAttributeCategory::USER => false, + CustomAttributeCategory::RESOURCE_TYPE => false, + ]); + } + public function ProcessDataRequest($dataRequest) { $this->presenter->HandleDataRequest($dataRequest); diff --git a/Pages/Admin/ManageResourcesPage.php b/Pages/Admin/ManageResourcesPage.php index c137536d1..3e117b7e6 100644 --- a/Pages/Admin/ManageResourcesPage.php +++ b/Pages/Admin/ManageResourcesPage.php @@ -1323,7 +1323,7 @@ public function AsFilter($customAttributes) $idEquals = new SqlFilterEquals($attributeId, $id); $f->AppendSql('LEFT JOIN `' . TableNames::CUSTOM_ATTRIBUTE_VALUES . '` `a' . $id . '` ON `a0`.`entity_id` = `a' . $id . '`.`entity_id` '); - if ($attribute->Type() == CustomAttributeTypes::MULTI_LINE_TEXTBOX || $attribute->Type() == CustomAttributeTypes::SINGLE_LINE_TEXTBOX) { + if ((int)$attribute->Type() === CustomAttributeTypes::MULTI_LINE_TEXTBOX || (int)$attribute->Type() === CustomAttributeTypes::SINGLE_LINE_TEXTBOX) { $attributeFragment->_And($idEquals->_And(new SqlFilterLike($attributeValue, $value))); } else { $attributeFragment->_And($idEquals->_And(new SqlFilterEquals($attributeValue, $value))); diff --git a/Presenters/Admin/ManageAttributesPresenter.php b/Presenters/Admin/ManageAttributesPresenter.php index ce8639704..6e3d7eeed 100644 --- a/Presenters/Admin/ManageAttributesPresenter.php +++ b/Presenters/Admin/ManageAttributesPresenter.php @@ -36,6 +36,8 @@ public function __construct(IManageAttributesPage $page, IAttributeRepository $a public function PageLoad() { + // Set category visibility flags for template conditional rendering + $this->page->SetCategoryVisibilityRules(); } public function AddAttribute() diff --git a/Web/scripts/admin/attributes.js b/Web/scripts/admin/attributes.js index 930ef1a60..1e6d85393 100644 --- a/Web/scripts/admin/attributes.js +++ b/Web/scripts/admin/attributes.js @@ -29,7 +29,7 @@ function AttributeManagement(opts) { limitScope: $('.limitScope'), attributeSecondary: $('.attributeSecondary'), secondaryPrompt: $('.secondaryPrompt'), - secondaryAttributeCategory: $('.secondaryAttributeCategory ') + secondaryAttributeCategory: $('.secondaryAttributeCategory') }; function RefreshAttributeList() { @@ -52,7 +52,51 @@ function AttributeManagement(opts) { var updateEntityCallback = function () { }; + // Store template options for secondary categories + var secondaryCategoryOptions = {}; + + var updateSecondaryCategories = function() { + var primaryCategory = elements.attributeCategory.val(); + + // Update all secondary category selects + $('.secondaryAttributeCategory').each(function() { + var secondarySelect = $(this); + + // Clear and rebuild options based on primary category + secondarySelect.empty(); + + if (primaryCategory == options.categories.reservation) { + // Add user, resource, and resource_type options + secondarySelect.append(secondaryCategoryOptions.user); + secondarySelect.append(secondaryCategoryOptions.resource); + secondarySelect.append(secondaryCategoryOptions.resourceType); + } else if (primaryCategory == options.categories.resource) { + // Add resource and resource_type options + secondarySelect.append(secondaryCategoryOptions.resource); + secondarySelect.append(secondaryCategoryOptions.resourceType); + } + }); + }; + AttributeManagement.prototype.init = function () { + // Initialize visibility rules + initializeVisibilityRules(); + + // Initialize secondary category options from template + var templateSelect = $('#attributeSecondaryCategory'); + if (templateSelect.length > 0) { + templateSelect.find('option').each(function() { + var optionValue = $(this).val(); + var optionText = $(this).text(); + if (optionValue == options.categories.user) { + secondaryCategoryOptions.user = ''; + } else if (optionValue == options.categories.resource) { + secondaryCategoryOptions.resource = ''; + } else if (optionValue == options.categories.resource_type) { + secondaryCategoryOptions.resourceType = ''; + } + }); + } $(".save").click(function () { $(this).closest('form').submit(); @@ -63,12 +107,30 @@ function AttributeManagement(opts) { }); RefreshAttributeList(); + updateSecondaryCategories(); elements.attributeCategory.change(function () { RefreshAttributeList(); + updateSecondaryCategories(); + // Reset form state when category changes + elements.limitScope.prop('checked', false); + elements.attributeSecondary.addClass('d-none'); + showRelevantCategoryOptions(); + }); + + // Bind field visibility events + elements.attributeCategory.on('change', updateFieldVisibility); + $('.limitScope').on('change', updateScopeVisibility); + + // Initialize visibility on load + updateFieldVisibility(); + updateScopeVisibility(); + + $(".cancel").click(function () { + $(this).closest('.dialog').dialog("close"); }); - elements.attributeList.on( 'click', 'a.update', function (e) { + elements.attributeList.on('click', 'a.update', function (e) { e.preventDefault(); e.stopPropagation(); }); @@ -130,7 +192,11 @@ function AttributeManagement(opts) { e.preventDefault(); activeAppliesTo = $(this); - showEntities($(this), $(this).closest('.textBoxOptions').find('.secondaryAttributeCategory').val(), currentAttributeEntities.secondaryEntityIds, 'ATTRIBUTE_SECONDARY_ENTITY_IDS'); + // Determine which dialog we're in and get the appropriate secondary category value + var dialog = $(this).closest('.modal-content'); + var secondaryCategory = dialog.find('.secondaryAttributeCategory').val(); + + showEntities($(this), secondaryCategory, currentAttributeEntities.secondaryEntityIds, 'ATTRIBUTE_SECONDARY_ENTITY_IDS'); updateEntityCallback = function (selectedIds) { currentAttributeEntities.secondaryEntityIds = selectedIds; @@ -155,10 +221,21 @@ function AttributeManagement(opts) { handleEntitiesSelected(activeAppliesTo); }); + // Handle scope limiting checkbox - show/hide secondary options elements.limitScope.change(function () { - elements.attributeSecondary.addClass('d-none'); - if (elements.limitScope.is(':checked')) { - elements.attributeSecondary.removeClass('d-none'); + if ($(this).is(':checked')) { + elements.attributeSecondary.removeClass('d-none').show(); + updateSecondaryCategories(); + } else { + elements.attributeSecondary.addClass('d-none').hide(); + // Reset secondary selections when unchecked + currentAttributeEntities.secondaryEntityIds = []; + elements.secondaryPrompt.text(opts.allText); + } + + // Call template's scope visibility function if it exists + if (typeof updateScopeVisibility === 'function') { + updateScopeVisibility(); } }); @@ -224,12 +301,18 @@ function AttributeManagement(opts) { elements.addForm.resetForm(); elements.addDialog.modal('hide'); RefreshAttributeList(); + + // Reset visibility after add + resetFormVisibility(); }; var editAttributeHandler = function () { elements.form.resetForm(); elements.editDialog.modal('hide'); RefreshAttributeList(); + + // Reset visibility after edit + resetFormVisibility(); }; var deleteAttributeHandler = function () { @@ -237,9 +320,71 @@ function AttributeManagement(opts) { RefreshAttributeList(); }; + // Visibility rules for different attribute categories + var visibilityRules = { + appliesTo: {}, + adminOnly: {}, + isPrivate: {}, + secondaryEntities: {} + }; + + // Initialize visibility rules from options + var initializeVisibilityRules = function() { + if (options.visibilityRules) { + visibilityRules = options.visibilityRules; + } + }; + + // Update field visibility based on selected category + var updateFieldVisibility = function() { + var selectedCategory = elements.attributeCategory.val(); + + // Show/hide fields based on rules + $('.attributeUnique').toggle(visibilityRules.appliesTo[selectedCategory] === true); + $('.attributeAdminOnly').toggle(visibilityRules.adminOnly[selectedCategory] === true); + $('.attributeIsPrivate').toggle(visibilityRules.isPrivate[selectedCategory] === true); + $('.secondaryEntities').toggle(visibilityRules.secondaryEntities[selectedCategory] === true); + + if (!visibilityRules.secondaryEntities[selectedCategory]) { + $('.limitScope').prop('checked', false); + $('.attributeSecondary').hide(); + } + }; + + // Update scope visibility based on checkbox states + var updateScopeVisibility = function() { + $('.scope-conditional').each(function() { + var dependsOn = $(this).data('depends-on'); + if (dependsOn) { + var checkbox = $('#' + dependsOn); + if (checkbox.is(':checked')) { + $(this).removeClass('d-none').show(); + } else { + $(this).addClass('d-none').hide(); + } + } + }); + }; + + // Reset form visibility state after operations + var resetFormVisibility = function() { + updateFieldVisibility(); + updateScopeVisibility(); + + // Reset secondary selections + $('.limitScope').prop('checked', false); + $('.attributeSecondary').addClass('d-none').hide(); + elements.secondaryPrompt.text(options.allText); + }; + + // Make functions available globally for template compatibility + window.updateFieldVisibility = updateFieldVisibility; + window.updateScopeVisibility = updateScopeVisibility; + var showEditDialog = function (selectedAttribute) { showRelevantAttributeOptions(selectedAttribute.type, elements.editDialog); showRelevantCategoryOptions(); + updateSecondaryCategories(); // Ensure secondary categories are updated for the edit dialog $('.editAttributeType', elements.editDialog).hide(); $('#editType' + selectedAttribute.type).show(); @@ -282,6 +427,7 @@ function AttributeManagement(opts) { var limitScope = $('#editAttributeLimitScope'); limitScope.prop('checked', false); + $('.attributeSecondary').addClass('d-none').hide(); // Reset secondary visibility $('#editAttributeSecondaryEntityId').val(''); elements.secondaryPrompt.text(options.allText); @@ -297,6 +443,10 @@ function AttributeManagement(opts) { setActiveId(selectedAttribute.id); + // Update field and scope visibility for the edit dialog + updateFieldVisibility(); + updateScopeVisibility(); + elements.editDialog.modal('show'); }; @@ -317,20 +467,10 @@ function AttributeManagement(opts) { return elements.activeId.val(); } + // Field visibility is now controlled by PHP/template - just handle secondary entities var showRelevantCategoryOptions = function () { - if (elements.attributeCategory.val() == options.categories.reservation) { - $('.attributeUnique').hide(); - $('.attributeAdminOnly').show(); - $('.secondaryEntities, .attributeSecondary').addClass('d-none'); - $('.secondaryEntities').removeClass('d-none'); - $('.attributeIsPrivate').show(); - } - else { - $('.attributeUnique').show(); - //$('.attributeAdminOnly').hide(); - $('.secondaryEntities, .attributeSecondary').addClass('d-none'); - $('.attributeIsPrivate').hide(); - } + // Use the local field visibility function + updateFieldVisibility(); }; var showEntities = function (element, categoryId, selectedIds, formName) { diff --git a/WebServices/Controllers/AttributeSaveController.php b/WebServices/Controllers/AttributeSaveController.php index 9a221e3fd..76da161cc 100644 --- a/WebServices/Controllers/AttributeSaveController.php +++ b/WebServices/Controllers/AttributeSaveController.php @@ -169,24 +169,26 @@ private function ValidateRequest($request) $errors[] = 'label is required'; } - if ($request->type != CustomAttributeTypes::CHECKBOX && - $request->type != CustomAttributeTypes::MULTI_LINE_TEXTBOX && - $request->type != CustomAttributeTypes::SELECT_LIST && - $request->type != CustomAttributeTypes::SINGLE_LINE_TEXTBOX + if ((int)$request->type !== CustomAttributeTypes::CHECKBOX && + (int)$request->type !== CustomAttributeTypes::MULTI_LINE_TEXTBOX && + (int)$request->type !== CustomAttributeTypes::SELECT_LIST && + (int)$request->type !== CustomAttributeTypes::SINGLE_LINE_TEXTBOX && + (int)$request->type !== CustomAttributeTypes::DATETIME ) { $errors[] = sprintf( - 'type is invalid. Allowed values for type: %s (checkbox), %s (multi line), %s (select list), %s (single line)', + 'type is invalid. Allowed values for type: %s (checkbox), %s (multi line), %s (select list), %s (single line), %s (datetime)', CustomAttributeTypes::CHECKBOX, CustomAttributeTypes::MULTI_LINE_TEXTBOX, CustomAttributeTypes::SELECT_LIST, - CustomAttributeTypes::SINGLE_LINE_TEXTBOX + CustomAttributeTypes::SINGLE_LINE_TEXTBOX, + CustomAttributeTypes::DATETIME ); } - if ($request->categoryId != CustomAttributeCategory::RESERVATION && - $request->categoryId != CustomAttributeCategory::RESOURCE && - $request->categoryId != CustomAttributeCategory::RESOURCE_TYPE && - $request->categoryId != CustomAttributeCategory::USER + if ((int)$request->categoryId !== CustomAttributeCategory::RESERVATION && + (int)$request->categoryId !== CustomAttributeCategory::RESOURCE && + (int)$request->categoryId !== CustomAttributeCategory::RESOURCE_TYPE && + (int)$request->categoryId !== CustomAttributeCategory::USER ) { $errors[] = sprintf( 'categoryId is invalid. Allowed values for category: %s (reservation), %s (resource), %s (resource type), %s (user)', @@ -197,15 +199,15 @@ private function ValidateRequest($request) ); } - if ($request->type == CustomAttributeTypes::SELECT_LIST && empty($request->possibleValues)) { + if ((int)$request->type === CustomAttributeTypes::SELECT_LIST && empty($request->possibleValues)) { $errors[] = 'possibleValues is required when the type is a select list'; } - if ($request->type != CustomAttributeTypes::SELECT_LIST && !empty($request->possibleValues)) { + if ((int)$request->type !== CustomAttributeTypes::SELECT_LIST && !empty($request->possibleValues)) { $errors[] = 'possibleValues is only valid when the type is a select list'; } - if ($request->categoryId == CustomAttributeCategory::RESERVATION && !empty($request->appliesToIds)) { + if ((int)$request->categoryId === CustomAttributeCategory::RESERVATION && !empty($request->appliesToIds)) { $errors[] = 'appliesToId is not valid when the type is reservation'; } diff --git a/lang/de_de.php b/lang/de_de.php index 71c38bce2..071300dea 100644 --- a/lang/de_de.php +++ b/lang/de_de.php @@ -463,6 +463,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Regel für Reservierungsfarbe hinzufügen'; $strings['LimitAttributeScope'] = 'In speziellen Fällen sammeln'; $strings['CollectFor'] = 'Sammeln für'; + $strings['AdminOnlyHelp'] = 'Nur Administratoren können diesen Attributwert einsehen und bearbeiten'; + $strings['PrivateHelp'] = 'Dieser Attributwert ist nur für den Besitzer der Reservierung und Administratoren sichtbar'; + $strings['LimitAttributeScopeHelp'] = 'Dieses Attribut nur für bestimmte Entitäten sammeln, anstatt für alle Entitäten dieser Kategorie'; $strings['SignIn'] = 'Anmelden'; $strings['AllParticipants'] = 'Alle Teilnehmer'; $strings['RegisterANewAccount'] = 'Ein neues Benutzerkonto registrieren'; diff --git a/lang/du_nl.php b/lang/du_nl.php index bd736425f..4372dc1df 100644 --- a/lang/du_nl.php +++ b/lang/du_nl.php @@ -472,6 +472,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Voeg reserverings kleur regel toe'; $strings['LimitAttributeScope'] = 'Verzamel in specifieke gevallen'; $strings['CollectFor'] = 'Verzamel voor'; + $strings['AdminOnlyHelp'] = 'Alleen beheerders kunnen deze attribuutwaarde bekijken en bewerken'; + $strings['PrivateHelp'] = 'Deze attribuutwaarde is alleen zichtbaar voor de gebruiker die de reservering bezit en beheerders'; + $strings['LimitAttributeScopeHelp'] = 'Verzamel dit attribuut alleen voor specifieke entiteiten in plaats van alle entiteiten van deze categorie'; $strings['SignIn'] = 'Log in'; $strings['AllParticipants'] = 'Alle deelnemers'; $strings['RegisterANewAccount'] = 'Registreer een nieuw account'; diff --git a/lang/en_us.php b/lang/en_us.php index 6b0992367..5efc9ed3b 100644 --- a/lang/en_us.php +++ b/lang/en_us.php @@ -502,6 +502,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Add Reservation Color Rule'; $strings['LimitAttributeScope'] = 'Collect In Specific Cases'; $strings['CollectFor'] = 'Collect For'; + $strings['AdminOnlyHelp'] = 'Only administrators can see and edit this attribute value'; + $strings['PrivateHelp'] = 'This attribute value is only visible to the user who owns the reservation and administrators'; + $strings['LimitAttributeScopeHelp'] = 'Collect this attribute only for specific entities instead of all entities of this category'; $strings['SignIn'] = 'Sign In'; $strings['SignInWith'] = 'Sign in with'; $strings['AllParticipants'] = 'All Participants'; diff --git a/lang/es.php b/lang/es.php index a73db3093..4c8ec3fbd 100644 --- a/lang/es.php +++ b/lang/es.php @@ -465,6 +465,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Agregar regla de color de reserva'; $strings['LimitAttributeScope'] = 'Recopilar en casos específicos'; $strings['CollectFor'] = 'Recopilar para'; + $strings['AdminOnlyHelp'] = 'Solo los administradores pueden ver y editar este valor de atributo'; + $strings['PrivateHelp'] = 'Este valor de atributo solo es visible para el usuario que posee la reserva y los administradores'; + $strings['LimitAttributeScopeHelp'] = 'Recopilar este atributo solo para entidades específicas en lugar de todas las entidades de esta categoría'; $strings['SignIn'] = 'Iniciar sesión'; $strings['SignInWith'] = 'Iniciar sesión con'; $strings['AllParticipants'] = 'Todos los participantes'; diff --git a/lang/fr_fr.php b/lang/fr_fr.php index d311568d2..ce0e7b6ca 100644 --- a/lang/fr_fr.php +++ b/lang/fr_fr.php @@ -465,6 +465,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Ajouter une Règle de Couleur pour les Réservations'; $strings['LimitAttributeScope'] = 'Afficher dans des Cas Spécifiques'; $strings['CollectFor'] = 'Afficher pour'; + $strings['AdminOnlyHelp'] = 'Seuls les administrateurs peuvent voir et modifier cette valeur d\'attribut'; + $strings['PrivateHelp'] = 'Cette valeur d\'attribut n\'est visible que pour l\'utilisateur qui possède la réservation et les administrateurs'; + $strings['LimitAttributeScopeHelp'] = 'Collecter cet attribut uniquement pour des entités spécifiques plutôt que pour toutes les entités de cette catégorie'; $strings['SignIn'] = 'Connexion'; $strings['AllParticipants'] = 'Tous les Participants'; $strings['RegisterANewAccount'] = 'Enregistrer un Nouveau Compte'; diff --git a/lang/it_it.php b/lang/it_it.php index 8ff8ec1a7..f74ceb06d 100644 --- a/lang/it_it.php +++ b/lang/it_it.php @@ -498,8 +498,11 @@ protected function _LoadStrings() $strings['RequiredValue'] = 'Obbligatori'; $strings['ReservationCustomRuleAdd'] = 'Se %s allora la prenotazione apparirà in'; $strings['AddReservationColorRule'] = 'Aggiungi un colore di ruolo per la prenotazione'; - $strings['LimitAttributeScope'] = 'Limitata l'attributo a...'; + $strings['LimitAttributeScope'] = 'Limitata l\'attributo a...'; $strings['CollectFor'] = 'Limitato a'; + $strings['AdminOnlyHelp'] = 'Solo gli amministratori possono vedere e modificare questo valore di attributo'; + $strings['PrivateHelp'] = 'Questo valore di attributo è visibile solo all\'utente che possiede la prenotazione e agli amministratori'; + $strings['LimitAttributeScopeHelp'] = 'Raccogliere questo attributo solo per entità specifiche invece di tutte le entità di questa categoria'; $strings['SignIn'] = 'Iscriviti'; $strings['AllParticipants'] = 'Tutti i partecipanti'; $strings['RegisterANewAccount'] = 'Crea un nuovo profilo'; diff --git a/lang/ja_jp.php b/lang/ja_jp.php index c431f553f..d0f931e63 100644 --- a/lang/ja_jp.php +++ b/lang/ja_jp.php @@ -502,6 +502,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = '予約の背景色を変更'; $strings['LimitAttributeScope'] = '特定の場合のみ取得'; $strings['CollectFor'] = '次の予約時:'; + $strings['AdminOnlyHelp'] = '管理者のみがこの属性値を表示および編集できます'; + $strings['PrivateHelp'] = 'この属性値は、予約の所有者と管理者のみに表示されます'; + $strings['LimitAttributeScopeHelp'] = 'このカテゴリのすべてのエンティティではなく、特定のエンティティのみでこの属性を収集します'; $strings['SignIn'] = 'サインイン'; $strings['AllParticipants'] = '全ての参加者'; $strings['RegisterANewAccount'] = '新しいアカウントを登録する'; diff --git a/lang/pt_br.php b/lang/pt_br.php index 54016d8b9..9067cad05 100644 --- a/lang/pt_br.php +++ b/lang/pt_br.php @@ -464,6 +464,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Adicionar regra de cor de reserva'; $strings['LimitAttributeScope'] = 'Coletar em casos específicos'; $strings['CollectFor'] = 'Recolher para'; + $strings['AdminOnlyHelp'] = 'Apenas administradores podem ver e editar este valor de atributo'; + $strings['PrivateHelp'] = 'Este valor de atributo é visível apenas para o usuário que possui a reserva e administradores'; + $strings['LimitAttributeScopeHelp'] = 'Coletar este atributo apenas para entidades específicas em vez de todas as entidades desta categoria'; $strings['SignIn'] = 'Entrar'; $strings['AllParticipants'] = 'Todos os participantes'; $strings['RegisterANewAccount'] = 'Registrar Nova Conta'; diff --git a/lang/ru_ru.php b/lang/ru_ru.php index 8f3b3b31e..6e8523ea8 100644 --- a/lang/ru_ru.php +++ b/lang/ru_ru.php @@ -472,6 +472,9 @@ protected function _LoadStrings() $strings['AddReservationColorRule'] = 'Добавить правило окраса бронирования'; $strings['LimitAttributeScope'] = 'Сбор по конкретным случаям'; $strings['CollectFor'] = 'Собрать для'; + $strings['AdminOnlyHelp'] = 'Только администраторы могут просматривать и редактировать это значение атрибута'; + $strings['PrivateHelp'] = 'Это значение атрибута видно только пользователю, который владеет бронированием, и администраторам'; + $strings['LimitAttributeScopeHelp'] = 'Собирать этот атрибут только для конкретных объектов, а не для всех объектов этой категории'; $strings['SignIn'] = 'Войти в систему'; $strings['AllParticipants'] = 'Все участники'; $strings['RegisterANewAccount'] = 'Регистрация новой учетной записи'; diff --git a/lib/Application/Attributes/AttributeService.php b/lib/Application/Attributes/AttributeService.php index 31fbe4322..a596de64a 100644 --- a/lib/Application/Attributes/AttributeService.php +++ b/lib/Application/Attributes/AttributeService.php @@ -42,6 +42,14 @@ public function GetById($attributeId); * @return Attribute[] */ public function GetReservationAttributes(UserSession $userSession, ReservationView $reservationView, $requestedUserId = 0, $requestedResourceIds = []); + + /** + * @param UserSession $userSession + * @param int $resourceId + * @param int $resourceTypeId + * @return Attribute[] + */ + public function GetResourceAttributes(UserSession $userSession, $resourceId = 0, $resourceTypeId = 0); } class AttributeService implements IAttributeService @@ -165,27 +173,32 @@ public function Validate($category, $attributeValues, $entityIds = [], $ignoreEm $attributes = $this->attributeRepository->GetByCategory($category); foreach ($attributes as $attribute) { + // Skip if attribute doesn't apply to this entity if (!empty($entityIds) && (($attribute->UniquePerEntity() && count(array_intersect($entityIds, $attribute->EntityIds())) == 0) || ($attribute->HasSecondaryEntities() && count(array_intersect($entityIds, $attribute->SecondaryEntityIds())) == 0))) { continue; } + // Skip admin-only attributes for non-admins if ($attribute->AdminOnly() && !$isAdmin) { continue; } + // Skip if attribute is not required and no value provided if (!$attribute->Required() && !array_key_exists($attribute->Id(), $values)) { continue; } - $value = trim($values[$attribute->Id()]); + $value = array_key_exists($attribute->Id(), $values) ? trim($values[$attribute->Id()]) : ''; $label = $attribute->Label(); + // Allow empty values for admins when ignoreEmpty is true if (empty($value) && ($ignoreEmpty || $isAdmin)) { continue; } + // Validate required fields if (!$attribute->SatisfiesRequired($value)) { $isValid = false; $error = $resources->GetString('CustomAttributeRequired', $label); @@ -193,7 +206,8 @@ public function Validate($category, $attributeValues, $entityIds = [], $ignoreEm $invalidAttributes[] = new InvalidAttribute($attribute, $error); } - if (!$attribute->SatisfiesConstraint($value)) { + // Validate field constraints (regex, type validation, etc.) + if (!empty($value) && !$attribute->SatisfiesConstraint($value)) { $isValid = false; $error = $resources->GetString('CustomAttributeInvalid', $label); $errors[] = $error; @@ -231,9 +245,9 @@ public function GetReservationAttributes(UserSession $userSession, ReservationVi $secondaryCategory = $attribute->SecondaryCategory(); if (empty($secondaryCategory) || ( - $secondaryCategory == CustomAttributeCategory::USER && + (int)$secondaryCategory === CustomAttributeCategory::USER && $this->AvailableForUser($userSession, $requestedUserId, $secondaryCategory, $attribute) || - (($secondaryCategory == CustomAttributeCategory::RESOURCE || $secondaryCategory == CustomAttributeCategory::RESOURCE_TYPE) + (((int)$secondaryCategory === CustomAttributeCategory::RESOURCE || (int)$secondaryCategory === CustomAttributeCategory::RESOURCE_TYPE) && $this->AvailableForResource($userSession, $secondaryCategory, $attribute, $requestedResourceIds)) ) ) { @@ -248,6 +262,50 @@ public function GetReservationAttributes(UserSession $userSession, ReservationVi return $attributes; } + public function GetResourceAttributes(UserSession $userSession, $resourceId = 0, $resourceTypeId = 0) + { + $attributes = []; + $customAttributes = $this->GetByCategory(CustomAttributeCategory::RESOURCE); + + foreach ($customAttributes as $attribute) { + $secondaryCategory = $attribute->SecondaryCategory(); + $shouldInclude = false; + + if (empty($secondaryCategory)) { + // No secondary filtering, but don't include here - these are handled statically + continue; + } else { + if ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE_TYPE && $resourceTypeId > 0) { + // Check if this attribute applies to the specific resource type + $shouldInclude = in_array($resourceTypeId, $attribute->SecondaryEntityIds()); + } elseif ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE && $resourceId > 0) { + // Check if this attribute applies to the specific resource + $shouldInclude = in_array($resourceId, $attribute->SecondaryEntityIds()); + + // Also check if user has permission to see this resource + if ($shouldInclude) { + $allowedResources = $this->GetAllowedResources($userSession); + $shouldInclude = array_key_exists($resourceId, $allowedResources); + } + } + } + + if ($shouldInclude) { + // Check admin-only attributes + $viewableForAdmin = (!$attribute->AdminOnly() || ($attribute->AdminOnly() && $userSession->IsAdmin)); + + // Check private attributes + $viewableForPrivate = (!$attribute->IsPrivate() || ($attribute->IsPrivate() && $userSession->IsAdmin)); + + if ($viewableForAdmin && $viewableForPrivate) { + $attributes[] = new LBAttribute($attribute, null); + } + } + } + + return $attributes; + } + private function CanReserveFor(UserSession $userSession, $requestedUserId) { return $userSession->UserId == $requestedUserId || $this->GetAuthorizationService()->CanReserveFor($userSession, $requestedUserId); @@ -268,6 +326,7 @@ private function AvailableForUser(UserSession $userSession, $requestedUserId, $s } /** + * Check if attribute is available for the requested resources * @param UserSession $userSession * @param string $secondaryCategory * @param CustomAttribute $attribute @@ -276,12 +335,15 @@ private function AvailableForUser(UserSession $userSession, $requestedUserId, $s */ private function AvailableForResource($userSession, $secondaryCategory, $attribute, $requestedResourceIds) { - if ($secondaryCategory == CustomAttributeCategory::RESOURCE || $secondaryCategory == CustomAttributeCategory::RESOURCE_TYPE) { - if ($secondaryCategory == CustomAttributeCategory::RESOURCE) { + if ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE || (int)$secondaryCategory === CustomAttributeCategory::RESOURCE_TYPE) { + if ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE) { $applies = array_intersect($attribute->SecondaryEntityIds(), $requestedResourceIds); $allowed = array_intersect($attribute->SecondaryEntityIds(), array_keys($this->GetAllowedResources($userSession))); - Log::Debug('applies %s allowed %s, ids %s requested %s', count($applies), count($allowed), join(',', $attribute->SecondaryEntityIds()), join(',', $requestedResourceIds)); + Log::Debug('Resource attribute check: applies %s allowed %s, attribute_ids %s requested %s', + count($applies), count($allowed), + join(',', $attribute->SecondaryEntityIds()), + join(',', $requestedResourceIds)); return count($applies) > 0 && count($allowed) > 0; } @@ -294,11 +356,14 @@ private function AvailableForResource($userSession, $secondaryCategory, $attribu $resource = $allowedResources[$resourceId]; if (in_array($resource->GetResourceType(), $attribute->SecondaryEntityIds())) { + Log::Debug('Resource type attribute matches: resource %s type %s', + $resourceId, $resource->GetResourceType()); return true; } } } + Log::Debug('Resource type attribute no match for resources %s', join(',', $requestedResourceIds)); return false; } } @@ -318,6 +383,74 @@ private function GetAllowedResources($userSession) return $this->allowedResources; } + + /** + * Validate reservation attributes (stub for interface compatibility) + * @param mixed $reservationSeries + * @param bool $isAdmin + * @return AttributeServiceValidationResult + */ + public function ValidateReservation($reservationSeries, $isAdmin) + { + // Stub implementation - extend as needed + return new AttributeServiceValidationResult(true, []); + } + + /** + * Get user managed attributes (stub for interface compatibility) + * @param int|null $userId + * @return array + */ + public function GetUserManagedAttributes($userId = null) + { + // Stub implementation - extend as needed + return []; + } + + /** + * Get user managed possible values (stub for interface compatibility) + * @param int $userId + * @param int $attributeId + * @return array + */ + public function GetUserManagedPossibleValues($userId, $attributeId) + { + // Stub implementation - extend as needed + return []; + } + + /** + * Update user managed possible values (stub for interface compatibility) + * @param int $userId + * @param int $attributeId + * @param string $valuesAsString + * @return void + */ + public function UpdateUserManagedPossibleValues($userId, $attributeId, $valuesAsString) + { + // Stub implementation - extend as needed + } + + /** + * Remove values missing dependencies (stub for interface compatibility) + * @param array $attributeValues + * @return array + */ + public function RemoveValuesMissingDependencies(array $attributeValues): array + { + // Stub implementation - extend as needed + return $attributeValues; + } + + /** + * Get time constrained attribute count (stub for interface compatibility) + * @return int + */ + public function GetTimeConstrainedAttributeCount(): int + { + // Stub implementation - extend as needed + return 0; + } } diff --git a/lib/Application/Reservation/Validation/CustomAttributeValidationRule.php b/lib/Application/Reservation/Validation/CustomAttributeValidationRule.php index ec5382d99..99c580523 100644 --- a/lib/Application/Reservation/Validation/CustomAttributeValidationRule.php +++ b/lib/Application/Reservation/Validation/CustomAttributeValidationRule.php @@ -38,15 +38,15 @@ public function Validate($reservationSeries, $retryParameters) $secondaryCategory = $invalidAttribute->Attribute->SecondaryCategory(); $secondaryEntityIds = $invalidAttribute->Attribute->SecondaryEntityIds(); - if ($secondaryCategory == CustomAttributeCategory::USER && !in_array($reservationSeries->UserId(), $secondaryEntityIds)) { + if ((int)$secondaryCategory === CustomAttributeCategory::USER && !in_array($reservationSeries->UserId(), $secondaryEntityIds)) { // the attribute applies to a different user continue; } - if ($secondaryCategory == CustomAttributeCategory::RESOURCE && count(array_intersect($secondaryEntityIds, $reservationSeries->AllResourceIds())) == 0) { + if ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE && count(array_intersect($secondaryEntityIds, $reservationSeries->AllResourceIds())) == 0) { // the attribute is not for a resource that is being booked continue; } - if ($secondaryCategory == CustomAttributeCategory::RESOURCE_TYPE) { + if ((int)$secondaryCategory === CustomAttributeCategory::RESOURCE_TYPE) { $appliesToResourceType = false; foreach ($reservationSeries->AllResources() as $resource) { if ($appliesToResourceType) { diff --git a/lib/Application/Schedule/ScheduleResourceFilter.php b/lib/Application/Schedule/ScheduleResourceFilter.php index 8a67d9bf1..197b61d72 100644 --- a/lib/Application/Schedule/ScheduleResourceFilter.php +++ b/lib/Application/Schedule/ScheduleResourceFilter.php @@ -160,9 +160,9 @@ public function FilterResources($resources, IResourceRepository $resourceReposit } /** - * @param Attribute[] $attributes + * @param LBAttribute[] $attributes * @param int $attributeId - * @return null|Attribute + * @return null|LBAttribute */ private function GetAttribute($attributes, $attributeId) { @@ -176,7 +176,7 @@ private function GetAttribute($attributes, $attributeId) /** * @param AttributeValue $attribute - * @param Attribute $value + * @param LBAttribute $value * @return bool */ private function AttributeValueMatches($attribute, $value) @@ -185,7 +185,7 @@ private function AttributeValueMatches($attribute, $value) return false; } - if ($value->Type() == CustomAttributeTypes::SINGLE_LINE_TEXTBOX || $value->Type() == CustomAttributeTypes::MULTI_LINE_TEXTBOX) { + if ((int)$value->Type() == CustomAttributeTypes::SINGLE_LINE_TEXTBOX || (int)$value->Type() == CustomAttributeTypes::MULTI_LINE_TEXTBOX) { return strripos($value->Value() ?? "", $attribute->Value) !== false; } elseif (is_numeric($value->Value())) { return floatval($value->Value()) == $attribute->Value; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2ad159eb2..8ddf2bd88 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -102,24 +102,6 @@ parameters: count: 1 path: lib/Application/Schedule/ReservationService.php - - - message: '#^Call to an undefined method Attribute\:\:Id\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Application/Schedule/ScheduleResourceFilter.php - - - - message: '#^Call to an undefined method Attribute\:\:Type\(\)\.$#' - identifier: method.notFound - count: 2 - path: lib/Application/Schedule/ScheduleResourceFilter.php - - - - message: '#^Call to an undefined method Attribute\:\:Value\(\)\.$#' - identifier: method.notFound - count: 4 - path: lib/Application/Schedule/ScheduleResourceFilter.php - - message: '#^PHPDoc tag @return with type Server is not subtype of native type IRestServer\|null\.$#' identifier: return.phpDocType diff --git a/tests/Presenters/Admin/ManageAttributesPresenterTest.php b/tests/Presenters/Admin/ManageAttributesPresenterTest.php index 3f0fbf572..5b1e80335 100644 --- a/tests/Presenters/Admin/ManageAttributesPresenterTest.php +++ b/tests/Presenters/Admin/ManageAttributesPresenterTest.php @@ -59,6 +59,7 @@ public function testAddsNewAttribute() $entityIds = [10]; $adminOnly = true; $secondaryEntityIds = ['1029', '2028']; + $isPrivate = false; $this->page->_label = $label; $this->page->_type = $type; @@ -72,9 +73,130 @@ public function testAddsNewAttribute() $this->page->_limitAttributeScope = true; $this->page->_secondaryCategory = CustomAttributeCategory::USER; $this->page->_secondaryEntityIds = $secondaryEntityIds; + $this->page->_isPrivate = $isPrivate; $expectedAttribute = CustomAttribute::Create($label, $type, $scope, $regex, $required, $possibleValues, $sortOrder, $entityIds, $adminOnly); $expectedAttribute->WithSecondaryEntities(CustomAttributeCategory::USER, $secondaryEntityIds); + $expectedAttribute->WithIsPrivate($isPrivate); + + $this->attributeRepository->expects($this->once()) + ->method('Add') + ->with($this->equalTo($expectedAttribute)) + ->willReturn(1); + + $this->presenter->AddAttribute(); + } + + public function testAddsNewAttributeWithoutSecondaryEntities() + { + $label = 'simple attribute'; + $scope = CustomAttributeCategory::USER; + $type = CustomAttributeTypes::SINGLE_LINE_TEXTBOX; + $required = false; + $regex = null; + $possibleValues = null; + $sortOrder = "2"; + $entityIds = [5, 10, 15]; + $adminOnly = false; + $isPrivate = false; + + $this->page->_label = $label; + $this->page->_type = $type; + $this->page->_category = $scope; + $this->page->_required = $required; + $this->page->_regex = $regex; + $this->page->_possibleValues = $possibleValues; + $this->page->_sortOrder = $sortOrder; + $this->page->_entityIds = $entityIds; + $this->page->_adminOnly = $adminOnly; + $this->page->_limitAttributeScope = false; // No secondary entities + $this->page->_secondaryCategory = null; + $this->page->_secondaryEntityIds = []; + $this->page->_isPrivate = $isPrivate; + + $expectedAttribute = CustomAttribute::Create($label, $type, $scope, $regex, $required, $possibleValues, $sortOrder, $entityIds, $adminOnly); + // No secondary entities when limitAttributeScope is false + $expectedAttribute->WithIsPrivate($isPrivate); + + $this->attributeRepository->expects($this->once()) + ->method('Add') + ->with($this->equalTo($expectedAttribute)) + ->willReturn(1); + + $this->presenter->AddAttribute(); + } + + public function testAddsNewAttributeWithSecondaryResources() + { + $label = 'resource attribute'; + $scope = CustomAttributeCategory::RESERVATION; + $type = CustomAttributeTypes::SELECT_LIST; + $required = false; + $regex = null; + $possibleValues = 'option1,option2,option3'; + $sortOrder = "3"; + $entityIds = []; + $adminOnly = false; + $secondaryEntityIds = ['101', '102', '103']; // Resource IDs + $isPrivate = false; + + $this->page->_label = $label; + $this->page->_type = $type; + $this->page->_category = $scope; + $this->page->_required = $required; + $this->page->_regex = $regex; + $this->page->_possibleValues = $possibleValues; + $this->page->_sortOrder = $sortOrder; + $this->page->_entityIds = $entityIds; + $this->page->_adminOnly = $adminOnly; + $this->page->_limitAttributeScope = true; + $this->page->_secondaryCategory = CustomAttributeCategory::RESOURCE; + $this->page->_secondaryEntityIds = $secondaryEntityIds; + $this->page->_isPrivate = $isPrivate; + + $expectedAttribute = CustomAttribute::Create($label, $type, $scope, $regex, $required, $possibleValues, $sortOrder, $entityIds, $adminOnly); + $expectedAttribute->WithSecondaryEntities(CustomAttributeCategory::RESOURCE, $secondaryEntityIds); + $expectedAttribute->WithIsPrivate($isPrivate); + + $this->attributeRepository->expects($this->once()) + ->method('Add') + ->with($this->equalTo($expectedAttribute)) + ->willReturn(1); + + $this->presenter->AddAttribute(); + } + + public function testAddsNewAttributeWithSecondaryResourceTypes() + { + $label = 'resource type attribute'; + $scope = CustomAttributeCategory::RESERVATION; + $type = CustomAttributeTypes::MULTI_LINE_TEXTBOX; + $required = true; + $regex = null; + $possibleValues = null; + $sortOrder = "1"; + $entityIds = []; + $adminOnly = true; + $secondaryEntityIds = ['201', '202']; // Resource Type IDs + $isPrivate = true; + + $this->page->_label = $label; + $this->page->_type = $type; + $this->page->_category = $scope; + $this->page->_required = $required; + $this->page->_regex = $regex; + $this->page->_possibleValues = $possibleValues; + $this->page->_sortOrder = $sortOrder; + $this->page->_entityIds = $entityIds; + $this->page->_adminOnly = $adminOnly; + $this->page->_limitAttributeScope = true; + $this->page->_secondaryCategory = CustomAttributeCategory::RESOURCE_TYPE; + $this->page->_secondaryEntityIds = $secondaryEntityIds; + $this->page->_isPrivate = $isPrivate; + + $expectedAttribute = CustomAttribute::Create($label, $type, $scope, $regex, $required, $possibleValues, $sortOrder, $entityIds, $adminOnly); + $expectedAttribute->WithSecondaryEntities(CustomAttributeCategory::RESOURCE_TYPE, $secondaryEntityIds); + $expectedAttribute->WithIsPrivate($isPrivate); $this->attributeRepository->expects($this->once()) ->method('Add') @@ -135,6 +257,57 @@ public function testUpdatesAttribute() $this->assertEquals($isPrivate, $expectedAttribute->IsPrivate()); } + public function testUpdatesAttributeWithSecondaryResources() + { + $attributeId = 2092; + $label = 'updated resource attribute'; + $required = false; + $regex = '/^[A-Z]+$/'; + $possibleValues = 'High,Medium,Low'; + $sortOrder = "8"; + $entityIds = []; + $isPrivate = false; + $adminOnly = true; + $secondaryEntityIds = ['301', '302', '303']; + + $this->page->_label = $label; + $this->page->_required = $required; + $this->page->_regex = $regex; + $this->page->_possibleValues = $possibleValues; + $this->page->_attributeId = $attributeId; + $this->page->_sortOrder = $sortOrder; + $this->page->_entityIds = $entityIds; + $this->page->_adminOnly = $adminOnly; + $this->page->_limitAttributeScope = true; + $this->page->_secondaryCategory = CustomAttributeCategory::RESOURCE; + $this->page->_secondaryEntityIds = $secondaryEntityIds; + $this->page->_isPrivate = $isPrivate; + + $expectedAttribute = CustomAttribute::Create('old label', CustomAttributeTypes::SELECT_LIST, CustomAttributeCategory::RESERVATION, null, true, null, 1, [], false); + + $this->attributeRepository->expects($this->once()) + ->method('LoadById') + ->with($this->equalTo($attributeId)) + ->willReturn($expectedAttribute); + + $this->attributeRepository->expects($this->once()) + ->method('Update') + ->with($this->anything()); + + $this->presenter->UpdateAttribute(); + + $this->assertEquals($label, $expectedAttribute->Label()); + $this->assertEquals($regex, $expectedAttribute->Regex()); + $this->assertEquals($required, $expectedAttribute->Required()); + $this->assertEquals($possibleValues, $expectedAttribute->PossibleValues()); + $this->assertEquals($sortOrder, $expectedAttribute->SortOrder()); + $this->assertEquals([], $expectedAttribute->EntityIds(), 'cannot set entityids for reservation'); + $this->assertEquals($adminOnly, $expectedAttribute->AdminOnly()); + $this->assertEquals($secondaryEntityIds, $expectedAttribute->SecondaryEntityIds()); + $this->assertEquals(CustomAttributeCategory::RESOURCE, $expectedAttribute->SecondaryCategory()); + $this->assertEquals($isPrivate, $expectedAttribute->IsPrivate()); + } + public function testDeletesAttributeById() { $attributeId = 1091; @@ -252,4 +425,9 @@ public function GetIsPrivate() { return $this->_isPrivate; } + + public function SetCategoryVisibilityRules() + { + // Stub implementation for testing + } } diff --git a/tests/fakes/FakeAttributeService.php b/tests/fakes/FakeAttributeService.php index 841629436..271a94e59 100644 --- a/tests/fakes/FakeAttributeService.php +++ b/tests/fakes/FakeAttributeService.php @@ -17,6 +17,11 @@ class FakeAttributeService implements IAttributeService public $_ByCategory = []; public $_EntityAttributeList; + /** + * @var Attribute[] + */ + public $_ResourceAttributes = []; + /** * @param $category CustomAttributeCategory|int * @param $entityIds array|int[]|int @@ -69,4 +74,72 @@ public function GetReservationAttributes(UserSession $userSession, ReservationVi { return $this->_ReservationAttributes; } + + /** + * @param UserSession $userSession + * @param int $resourceId + * @param int $resourceTypeId + * @return Attribute[] + */ + public function GetResourceAttributes(UserSession $userSession, $resourceId = 0, $resourceTypeId = 0) + { + return $this->_ResourceAttributes; + } + + /** + * @param $reservationSeries + * @param $isAdmin + * @return mixed + */ + public function ValidateReservation($reservationSeries, $isAdmin) + { + return $this->_ValidationResult; + } + + /** + * @param $userId int|null + * @return array + */ + public function GetUserManagedAttributes($userId = null) + { + return []; + } + + /** + * @param $userId + * @param $attributeId + * @return array + */ + public function GetUserManagedPossibleValues($userId, $attributeId) + { + return []; + } + + /** + * @param $userId + * @param $attributeId + * @param $valuesAsString + * @return void + */ + public function UpdateUserManagedPossibleValues($userId, $attributeId, $valuesAsString) + { + // Stub implementation + } + + /** + * @param array $attributeValues + * @return array + */ + public function RemoveValuesMissingDependencies(array $attributeValues): array + { + return $attributeValues; + } + + /** + * @return int + */ + public function GetTimeConstrainedAttributeCount(): int + { + return 0; + } } diff --git a/tpl/Admin/Attributes/manage_attributes.tpl b/tpl/Admin/Attributes/manage_attributes.tpl index a073a8edd..fc7f69c14 100644 --- a/tpl/Admin/Attributes/manage_attributes.tpl +++ b/tpl/Admin/Attributes/manage_attributes.tpl @@ -61,9 +61,9 @@
@@ -72,8 +72,11 @@ maxlength=3 id="ATTRIBUTE_SORT_ORDER" />
+ {* Conditional form fields based on category - moved from JavaScript *} + + {* AppliesTo field - conditionally visible based on category *}
- + {translate key=All}
@@ -87,48 +90,61 @@ + {* AdminOnly field - conditionally visible based on category *}
+
+ + {* Private field - conditionally visible based on category *}
+
-
+ {* Secondary entities - conditionally visible based on category *} +
+
-
+ + {* Secondary category selection - show when limitScope is checked *} + -
+ + {* Secondary entity selection - show when limitScope is checked *} +
@@ -193,13 +209,16 @@ id="editAttributeSortOrder" />
-
- + {* Conditional form fields based on category - moved from JavaScript *} + + {* AppliesTo field - conditionally visible based on category *} +
+ {translate key=All}
-
+
@@ -208,47 +227,58 @@
-
+ {* AdminOnly field - conditionally visible based on category *} +
+
-
+ {* Private field - conditionally visible based on category *} +
+
-
+ {* Secondary entities - conditionally visible based on category *} +
+
-
+ {* Secondary category selection - show when limitScope is checked *} + -
+ {* Secondary entity selection - show when limitScope is checked *} + -{include file='globalfooter.tpl'} \ No newline at end of file +{include file='globalfooter.tpl'} diff --git a/tpl/Admin/Resources/manage_resources.tpl b/tpl/Admin/Resources/manage_resources.tpl index 596f36b01..2e31d92ac 100644 --- a/tpl/Admin/Resources/manage_resources.tpl +++ b/tpl/Admin/Resources/manage_resources.tpl @@ -566,34 +566,55 @@

- {if $AttributeList|default:array()|count > 0} - {assign var="hasResults" value=false} - {foreach from=$AttributeList item=attribute name=attrLoop} - {if $attribute->AppliesToEntity($id)} - {if !$hasResults} - {assign var="hasResults" value=true} - {* Content at the start of the iteration *} -
- {translate key='CustomAttributes'} - - - -
-
- {/if} - {include file='Admin/InlineAttributeEdit.tpl' id=$id attribute=$attribute value=$resource->GetAttributeValue($attribute->Id())} + {* Custom attributes section *} +
+ {translate key='CustomAttributes'} + + + +
+
+ {* All attributes - check resource type filtering first, then direct assignment *} + {foreach from=$AttributeList item=attribute name=attrLoop} + {assign var=showAttribute value=false} + + {* If attribute has secondary entities, check them based on category *} + {if $attribute->HasSecondaryEntities()} + {if $attribute->SecondaryCategory() == CustomAttributeCategory::RESOURCE_TYPE} + {* Check if resource's type matches any secondary entity (resource type) *} + {foreach from=$attribute->SecondaryEntityIds() item=secondaryId} + {if $secondaryId == $resource->GetResourceTypeId()} + {assign var=showAttribute value=true} + {break} + {/if} + {/foreach} + {elseif $attribute->SecondaryCategory() == CustomAttributeCategory::RESOURCE} + {* Check if this specific resource matches any secondary entity (resource) *} + {foreach from=$attribute->SecondaryEntityIds() item=secondaryId} + {if $secondaryId == $id} + {assign var=showAttribute value=true} + {break} + {/if} + {/foreach} {/if} - {/foreach} - {if $hasResults} - {* Content at the end of the iteration *} -
-
+ {* Otherwise, show if attribute applies directly to this specific resource *} + {elseif $attribute->AppliesToEntity($id)} + {assign var=showAttribute value=true} + {* Or show if attribute has no specific entity assignments (applies to all) *} + {elseif $attribute->EntityIds()|count == 0} + {assign var=showAttribute value=true} + {/if} + + {if $showAttribute} + {include file='Admin/InlineAttributeEdit.tpl' id=$id attribute=$attribute value=$resource->GetAttributeValue($attribute->Id())} + {/if} + {/foreach}
- {/if} - {/if} +
+
{translate key='Public'}
@@ -2215,7 +2236,11 @@ value:{$id}, text: "{$resourceType->Name()|escape:'javascript'}" }, {/foreach} - ] + ], + success: function(response, newValue) { + // Trigger custom event for dynamic attribute loading + $(this).trigger('save', [{ newValue: newValue }]); + } }); $('.sortOrderValue').editable({