From d93f443cfd7d69b4665cc8ec1bf8c828f7966409 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Mon, 14 Jul 2025 18:25:04 +0200 Subject: [PATCH 01/25] update inventory item terminology for clarity and consistency --- install/db_scripts/db.sql | 2 +- install/db_scripts/update_5_0.xml | 2 +- languages/en.xml | 46 +++++------ modules/inventory.php | 64 +++++++-------- src/Changelog/Service/ChangelogService.php | 2 +- src/Inventory/Service/ItemService.php | 10 +-- src/Inventory/ValueObjects/ItemsData.php | 90 +++++++++++----------- src/UI/Presenter/InventoryPresenter.php | 70 ++++++++--------- 8 files changed, 144 insertions(+), 142 deletions(-) diff --git a/install/db_scripts/db.sql b/install/db_scripts/db.sql index 3b0b57a2da..261c1d4808 100644 --- a/install/db_scripts/db.sql +++ b/install/db_scripts/db.sql @@ -1085,7 +1085,7 @@ CREATE TABLE %PREFIX%_inventory_items ini_uuid varchar(36) NOT NULL, ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, - ini_former boolean NOT NULL DEFAULT false, + ini_retired boolean NOT NULL DEFAULT false, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index 5dd0867508..e185ec6037 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -463,7 +463,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ini_uuid varchar(36) NOT NULL, ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, - ini_former boolean NOT NULL DEFAULT false, + ini_retired boolean NOT NULL DEFAULT false, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, diff --git a/languages/en.xml b/languages/en.xml index 69dee53a42..7bd8f73e17 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -810,8 +810,8 @@ Here a field of a consecutive number can be selected. In a selection, the current number is read out and increased according to the specified number.\n\nNOTE: InventoryManager can only tell if a data field is of type "number". Whether a serial number is stored in this data field can not be recognized. Filename The file name of the export file (without file extension). The file name is automatically appended with the export format’s extension. (Default: "Inventory manager") - Current - Former + In use + Retired In inventory Item is in inventory Here you can import items from a previous export file or your own file. @@ -829,15 +829,16 @@ Item successfully deleted Change the item Item successfully changed - Lend item + Lend the item Item lend data - Item was made to former - Make item former - You can #VAR1_BOLD#. This has the advantage that the data is preserved and you can later always see who has borrowed this item. - Undo made to former - Do you really want to undo the disposal of the item? - Undone item made to former - Return item + Reinstate the item + Do you really want to reinstate the item? + Item successfully reinstated + Retire the item + You can #VAR1_BOLD#. This has the advantage that the data is preserved and you can later always see who has borrowed this item. + Item successfully retired + Item retired? + Return the item Items Disable borrowing functionality When this option is enabled, items can no longer be borrowed. Existing borrowing records will still be saved, but will no longer be displayed. (Default: no) @@ -857,22 +858,22 @@ The name of the item Keeper Keeper of the item - You can reinstate the item into the inventory manager. This has the advantage that the data is preserved and you can later always see who has borrowed this item.\n\nIf you want to delete the item, please contact an administrator or the manager of the inventory manager! - You can reinstate the item into the inventory manager.\n\nIf you want to delete the item, please contact an administrator or the manager of the inventory manager!\n\n#VAR1# + You can reinstate the item into the inventory manager.\n\nIf you want to delete the item, please contact an administrator or the manager of the inventory manager!\n\n#VAR1# + You can retire the item from the inventory manager. This has the advantage that the data is preserved and you can later always see who has borrowed this item.\n\nIf you want to delete the item, please contact an administrator or the manager of the inventory manager! Last receiver Last receiver of the item There was no new data in the import file! The item #VAR1_BOLD# was changed by #VAR2_BOLD#: The item #VAR1_BOLD# was created by #VAR2_BOLD#: The following item was deleted: - The following item has been reinstated into the inventory: - The following item was made to former: + The following item has been reinstated into the inventory: + The following item has been retired: The following items were imported by #VAR1_BOLD#: An item in the inventory has been changed An item has been added to the inventory An item in the inventory has been deleted - An item has been reinstated to the inventory - An item in the inventory has been made to former + An item has been reinstated to the inventory + An item in the inventory has been retired Items have been imported into the inventory Number Number of items to be added @@ -887,15 +888,16 @@ The date the item was returned to the keeper Borrowed on The borrowing date of the item to the last recipient + Delete selected items If you select #VAR1_BOLD#, the items along with their associated records will be irrevocably removed from the database, and it will not be possible to view the data of these items later. The selected items have been deleted. Items successfully changed - The selected items have been marked as former. - Mark selection as former - You can also mark the selected items as former by choosing #VAR1_BOLD#. This has the advantage that the data is preserved and you can always later see who borrowed the item. - Undo made selection former - You can reinstate the selected items back into the inventory manager by choosing #VAR1_BOLD#. - The marking as former of the selected items has been undone. + Reinstate selected items + You can reinstate the selected items back into the inventory manager by choosing #VAR1_BOLD#. + Selected items successfully reinstated. + Retire selected items + You can retire the item from the inventory manager by choosing #VAR1_BOLD#. This has the advantage that the data is preserved and you can always later see who borrowed the item. + The selected items have been retired. Permission to edit the display name of system fields If this option is enabled, the display names of the system item fields can be modified. (Default: no) Current user as default selection diff --git a/modules/inventory.php b/modules/inventory.php index 4c8f889906..9f1cf4a93a 100644 --- a/modules/inventory.php +++ b/modules/inventory.php @@ -39,7 +39,7 @@ require(__DIR__ . '/../system/login_valid.php'); // Initialize and check the parameters - $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_lend', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_make_former', 'item_undo_former', 'item_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); + $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_lend', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_retire', 'item_reinstate', 'item_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); $getinfUUID = admFuncVariableIsValid($_GET, 'uuid', 'uuid'); $getOptionID = admFuncVariableIsValid($_GET, 'option_id', 'int', array('defaultValue' => 0)); $getFieldName = admFuncVariableIsValid($_GET, 'field_name', 'string', array('defaultValue' => "", 'directOutput' => true)); @@ -49,7 +49,7 @@ $postRedirect = admFuncVariableIsValid($_POST, 'redirect', 'numeric', array('defaultValue' => 1)); $postImported = admFuncVariableIsValid($_POST, 'imported', 'numeric', array('defaultValue' => 0)); $getCopy = admFuncVariableIsValid($_GET, 'copy', 'bool', array('defaultValue' => false)); - $getFormer = admFuncVariableIsValid($_GET, 'item_former', 'bool', array('defaultValue' => false)); + $getRetired = admFuncVariableIsValid($_GET, 'item_retired', 'bool', array('defaultValue' => false)); $getRedirectToImport = admFuncVariableIsValid($_GET, 'redirect_to_import', 'bool', array('defaultValue' => false)); $getItemUUIDs = admFuncVariableIsValid($_GET, 'item_uuids', 'array', array('defaultValue' => array())); if (empty($getItemUUIDs)) { @@ -263,13 +263,13 @@ case 'item_delete_explain_msg': if (count($getItemUUIDs) > 0) { - $undoFormerOnClick = 'callUrlHideElements(\'adm_inventory_item_\', [\'' . implode('\', \'', $getItemUUIDs) . '\'], \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_undo_former')) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; - $formerOnClick = 'callUrlHideElements(\'adm_inventory_item_\', [\'' . implode('\', \'', $getItemUUIDs) . '\'], \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_make_former')) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; + $reinstateOnClick = 'callUrlHideElements(\'adm_inventory_item_\', [\'' . implode('\', \'', $getItemUUIDs) . '\'], \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate')) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; + $retireOnClick = 'callUrlHideElements(\'adm_inventory_item_\', [\'' . implode('\', \'', $getItemUUIDs) . '\'], \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_retire')) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; $deleteOnClick = 'callUrlHideElements(\'adm_inventory_item_\', [\'' . implode('\', \'', $getItemUUIDs) . '\'], \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete')) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; $headerMsg = $gL10n->get('SYS_NOTE'); } else { - $formerOnClick = 'callUrlHideElement(\'adm_inventory_item_' . $getiniUUID . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_make_former', 'item_uuid' => $getiniUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; + $retireOnClick = 'callUrlHideElement(\'adm_inventory_item_' . $getiniUUID . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_retire', 'item_uuid' => $getiniUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; $deleteOnClick = 'callUrlHideElement(\'adm_inventory_item_' . $getiniUUID . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete', 'item_uuid' => $getiniUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')'; $headerMsg = $gL10n->get('SYS_INVENTORY_ITEM_DELETE'); } @@ -281,13 +281,13 @@ '; echo $msg; break; - case 'item_make_former': + case 'item_retire': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); if (count($getItemUUIDs) > 0) { foreach ($getItemUUIDs as $itemUuid) { $itemModule = new ItemService($gDb, $itemUuid); - $itemModule->makeItemFormer(); + $itemModule->retireItem(); } - echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_SELECTION_MADE_FORMER'))); + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_SELECTION_RETIRED'))); } else { $itemModule = new ItemService($gDb, $getiniUUID); - $itemModule->makeItemFormer(); - echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_MADE_FORMER'))); + $itemModule->retireItem(); + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_RETIRED'))); } break; - case 'item_undo_former': + case 'item_reinstate': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); if (count($getItemUUIDs) > 0) { foreach ($getItemUUIDs as $itemUuid) { $itemModule = new ItemService($gDb, $itemUuid); - $itemModule->undoItemFormer(); + $itemModule->reinstateItem(); } - echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_SELECTION_UNDONE_FORMER'))); + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_SELECTION_REINSTATED'))); } else { $itemModule = new ItemService($gDb, $getiniUUID); - $itemModule->undoItemFormer(); - echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_UNDONE_FORMER'))); + $itemModule->reinstateItem(); + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATED'))); } break; diff --git a/src/Changelog/Service/ChangelogService.php b/src/Changelog/Service/ChangelogService.php index 04937f9b0e..0a64dc194c 100644 --- a/src/Changelog/Service/ChangelogService.php +++ b/src/Changelog/Service/ChangelogService.php @@ -525,7 +525,7 @@ public static function getFieldTranslations(): array 'inf_required_input' => array('name' => 'SYS_REQUIRED_INPUT', 'type' => 'BOOL'), 'inf_sequence' => 'SYS_ORDER', 'ini_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), - 'ini_former' => array('name' => 'SYS_INVENTORY_ITEM_MADE_FORMER', 'type' => 'BOOL'), + 'ini_retired' => array('name' => 'SYS_INVENTORY_ITEM_RETIRED_CHANGELOG', 'type' => 'BOOL'), 'ind_value_bool' => array('name' => 'SYS_VALUE', 'type' => 'BOOL'), 'ind_value_date' => array('name' => 'SYS_VALUE', 'type' => 'DATE'), 'ind_value_mail' => array('name' => 'SYS_VALUE', 'type' => 'EMAIL'), diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index 5fcf361ee4..3eca1c574e 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -45,13 +45,13 @@ public function __construct(Database $database, string $itemUUID = '', int $post } /** - * Marks the item as former. + * Marks the item as retired. * * @throws Exception */ - public function makeItemFormer(): void + public function retireItem(): void { - $this->itemRessource->makeItemFormer(); + $this->itemRessource->retireItem(); // Send notification to all users $this->itemRessource->sendNotification(); @@ -61,9 +61,9 @@ public function makeItemFormer(): void * Reverts the item to its previous state. * @throws Exception */ - public function undoItemFormer(): void + public function reinstateItem(): void { - $this->itemRessource->undoItemFormer(); + $this->itemRessource->reinstateItem(); // Send notification to all users $this->itemRessource->sendNotification(); diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index 311941597f..953a2ae1ce 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -34,14 +34,14 @@ */ class ItemsData { - private bool $mItemCreated = false; ///< flag if a new item was created - private bool $mItemChanged = false; ///< flag if a new item was changed - private bool $mItemDeleted = false; ///< flag if a item was deleted - private bool $mItemMadeFormer = false; ///< flag if a item was made to former item - private bool $mItemUndoMadeFormer = false; ///< flag if a item was made to normal again - private bool $mItemImported = false; ///< flag if a item was imported - private bool $showFormerItems = true; ///< if true, than former items will be showed - private int $organizationId = -1; ///< ID of the organization for which the item field structure should be read + private bool $mItemCreated = false; ///< flag if a new item was created + private bool $mItemChanged = false; ///< flag if a new item was changed + private bool $mItemDeleted = false; ///< flag if a item was deleted + private bool $mItemRetired = false; ///< flag if a item was retired + private bool $mItemReinstated = false; ///< flag if a item was made to normal again + private bool $mItemImported = false; ///< flag if a item was imported + private bool $showRetiredItems = true; ///< if true, than retired items will be showed + private int $organizationId = -1; ///< ID of the organization for which the item field structure should be read private array $lendFieldNames = array('LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); ///< array with the internal field names of the lend fields /** @@ -230,11 +230,11 @@ public function readItems(): void $this->mItems = array(); $sqlWhereCondition = ''; - if (!$this->showFormerItems) { - $sqlWhereCondition .= 'AND ini_former = 0'; + if (!$this->showRetiredItems) { + $sqlWhereCondition .= 'AND ini_retired = 0'; } - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_former FROM ' . TBL_INVENTORY_ITEMS . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEMS . ' INNER JOIN ' . TBL_INVENTORY_ITEM_DATA . ' ON ind_ini_id = ini_id WHERE ini_org_id IS NULL @@ -243,7 +243,7 @@ public function readItems(): void $statement = $this->mDb->queryPrepared($sql, array($this->organizationId)); while ($row = $statement->fetch()) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_former' => $row['ini_former']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); } } @@ -261,8 +261,8 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void $this->mItems = array(); $sqlWhereCondition = ''; - if (!$this->showFormerItems) { - $sqlWhereCondition .= 'AND ini_former = 0'; + if (!$this->showRetiredItems) { + $sqlWhereCondition .= 'AND ini_retired = 0'; } $sqlImfIds = 'AND ('; @@ -274,7 +274,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void } // first read all item data for the given user - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_former FROM ' . TBL_INVENTORY_ITEM_DATA . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_DATA . ' INNER JOIN ' . TBL_INVENTORY_FIELDS . ' ON inf_id = ind_inf_id ' . $sqlImfIds . ' @@ -287,11 +287,11 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); while ($row = $statement->fetch()) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_former' => $row['ini_former']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); } // now read the item lend data for each item - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_former FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' INNER JOIN ' . TBL_INVENTORY_FIELDS . ' ON inf_id = inl_inf_id ' . $sqlImfIds . ' @@ -314,7 +314,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void } // if item doesn't exist, then add it to the items array if (!$itemExists) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_former' => $row['ini_former']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); } } } @@ -693,18 +693,18 @@ public function setImportedItem(): void } /** - * This method reads or stores the variable for showing former items. + * This method reads or stores the variable for showing retired items. * The values will be stored in database without any inspections! * - * @param bool|null $newValue If set, then the new value will be stored in @b showFormerItems. - * @return bool Returns the current value of @b showFormerItems + * @param bool|null $newValue If set, then the new value will be stored in @b showRetiredItems. + * @return bool Returns the current value of @b showRetiredItems */ - public function showFormerItems($newValue = null): bool + public function showRetiredItems($newValue = null): bool { if ($newValue !== null) { - $this->showFormerItems = $newValue; + $this->showRetiredItems = $newValue; } - return $this->showFormerItems; + return $this->showRetiredItems; } /** @@ -835,7 +835,7 @@ public function createNewItem(string $catUUID): void $newItem = new Item($this->mDb, $this, 0); $newItem->setValue('ini_org_id', $this->organizationId); - $newItem->setValue('ini_former', 0); + $newItem->setValue('ini_retired', 0); $newItem->setValue('ini_cat_id', $category->getValue('cat_id')); $newItem->save(); @@ -873,35 +873,35 @@ public function deleteItem(): void } /** - * Marks an item as former + * Marks an item as retired * - * @param int $itemId The ID of the item to be marked as former. + * @param int $itemId The ID of the item to be retired. * @return void */ - public function makeItemFormer(): void + public function retireItem(): void { $item = new Item($this->mDb, $this, $this->mItemId); - $item->setValue('ini_former', 1); + $item->setValue('ini_retired', 1); $item->save(); - $this->mItemMadeFormer = true; - $this->mItemUndoMadeFormer = false; + $this->mItemRetired = true; + $this->mItemReinstated = false; } /** - * Marks an item as no longer former + * Marks an item as reinstated which means it is no longer retired. * - * @param int $itemId The ID of the item to be marked as no longer former. + * @param int $itemId The ID of the item to be marked as reinstated. * @return void */ - public function undoItemFormer(): void + public function reinstateItem(): void { $item = new Item($this->mDb, $this, $this->mItemId); - $item->setValue('ini_former', 0); + $item->setValue('ini_retired', 0); $item->save(); - $this->mItemMadeFormer = false; - $this->mItemUndoMadeFormer = true; + $this->mItemRetired = false; + $this->mItemReinstated = true; } /** @@ -964,7 +964,7 @@ public function saveItemData(): void } /** - * Send a notification email that a new item was created, changed, deleted, or marked as former + * Send a notification email that a new item was created, changed, deleted, retired, reinstated or imported. * to all members of the notification role. This role is configured within the global preference * **system_notifications_role**. The email contains the item name, the name of the current user, * the timestamp, and the details of the changes. @@ -994,12 +994,12 @@ public function sendNotification($importData = null): bool } elseif ($this->mItemDeleted) { $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_DELETED'; $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_DELETED'; - } elseif ($this->mItemMadeFormer) { - $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_MADE_FORMER'; - $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_MADE_FORMER'; - } elseif ($this->mItemUndoMadeFormer) { - $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_UNDO_FORMER'; - $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_UNDO_FORMER'; + } elseif ($this->mItemRetired) { + $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_RETIRED'; + $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_RETIRED'; + } elseif ($this->mItemReinstated) { + $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_REINSTATED'; + $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_REINSTATED'; } elseif ($this->mItemChanged) { $messageTitleText = 'SYS_INVENTORY_NOTIFICATION_SUBJECT_ITEM_CHANGED'; $messageHead = 'SYS_INVENTORY_NOTIFICATION_MESSAGE_ITEM_CHANGED'; @@ -1009,7 +1009,7 @@ public function sendNotification($importData = null): bool // if items were imported then sent a message with all itemnames, the user and the date // if item was created or changed then sent a message with all changed fields in a table - // if item was deleted or made former then sent a message with the item name, the user and the date + // if item was deleted, retired or reinstated then sent a message with the item name, the user and the date if ($this->mItemImported || $this->mItemCreated || $this->mItemChanged) { $format_hdr = " %s %s %s \n"; $format_row = " %s %s %s \n"; diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index b2a40f7bce..e72c6b9f4e 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -62,7 +62,7 @@ class InventoryPresenter extends PagePresenter /** * @var bool true if all items should be shown */ - protected bool $showFormerItems = false; + protected bool $showRetiredItems = false; /** @@ -81,8 +81,8 @@ public function __construct() $this->itemsData = new ItemsData($gDb, $gCurrentOrgId); - $this->showFormerItems = ($this->getFilterItems >= 1) ? true : false; - $this->itemsData->showFormerItems($this->showFormerItems); + $this->showRetiredItems = ($this->getFilterItems >= 1) ? true : false; + $this->itemsData->showRetiredItems($this->showRetiredItems); $this->itemsData->readItems(); $this->categoryService = new CategoryService($gDb, 'IVT'); @@ -278,8 +278,8 @@ protected function createHeader(): void ); $selectBoxValues = array( - '0' => $gL10n->get('SYS_INVENTORY_FILTER_CURRENT_ITEMS'), - '1' => $gL10n->get('SYS_INVENTORY_FILTER_FORMER_ITEMS'), + '0' => $gL10n->get('SYS_INVENTORY_FILTER_IN_USE_ITEMS'), + '1' => $gL10n->get('SYS_INVENTORY_FILTER_RETIRED_ITEMS'), '2' => $gL10n->get('SYS_ALL') ); // filter all items @@ -752,7 +752,7 @@ public function prepareData(string $mode = 'html') : array $this->itemsData->readItemData($item['ini_uuid']); $rowValues = array(); $rowValues['item_uuid'] = $item['ini_uuid']; - $strikethrough = $item['ini_former']; + $strikethrough = $item['ini_retired']; $columnNumber = 1; foreach ($this->itemsData->getItemFields() as $itemField) { @@ -766,8 +766,8 @@ public function prepareData(string $mode = 'html') : array if ( ($this->getFilterCategoryUUID !== '' && $infNameIntern === 'CATEGORY' && $this->getFilterCategoryUUID != $this->itemsData->getValue($infNameIntern, 'database')) || ($this->getFilterKeeper !== 0 && $infNameIntern === 'KEEPER' && $this->getFilterKeeper != $this->itemsData->getValue($infNameIntern)) || - ($this->getFilterItems === 0 && $item['ini_former']) || - ($this->getFilterItems === 1 && !$item['ini_former']) + ($this->getFilterItems === 0 && $item['ini_retired']) || + ($this->getFilterItems === 1 && !$item['ini_retired']) ) { // skip to the next iteration of the next-outer loop continue 2; @@ -785,8 +785,8 @@ public function prepareData(string $mode = 'html') : array // Process ITEMNAME column if ($infNameIntern === 'ITEMNAME' && strlen($content) > 0) { - if ($mode === 'html' && (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_former'])) { - $content = '' . SecurityUtils::encodeHTML($content) . ''; + if ($mode === 'html' && (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_retired'])) { + $content = '' . SecurityUtils::encodeHTML($content) . ''; } else { $content = SecurityUtils::encodeHTML($content); } @@ -880,16 +880,16 @@ public function prepareData(string $mode = 'html') : array } if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_former']) { + if (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_retired']) { // Add edit action $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), 'icon' => 'bi bi-pencil-square', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); // Add lend action - if (!$item['ini_former'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { + if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { $itemLended = false; @@ -916,20 +916,20 @@ public function prepareData(string $mode = 'html') : array ); } - if ($item['ini_former']) { - $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_UNDO_FORMER_DESC', array('SYS_INVENTORY_ITEM_UNDO_FORMER_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_UNDO_FORMER_CONFIRM'); - // Add undo former action + if ($item['ini_retired']) { + $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); + // Add reinstate action $rowValues['actions'][] = array( - 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_undo_former', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', + 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', 'dataMessage' => $dataMessage, 'icon' => 'bi bi-eye', - 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_UNDO_FORMER') + 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE') ); } if ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (!$item['ini_former']) { - // Add make former action + if (!$item['ini_retired']) { + // Addretire action $rowValues['actions'][] = array( 'popup' => true, 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_keeper_explain_msg', 'item_uuid' => $item['ini_uuid'])), @@ -939,10 +939,10 @@ public function prepareData(string $mode = 'html') : array } } else { - // Add delete/make former action + // Add delete/retire action $rowValues['actions'][] = array( 'popup' => true, - 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])), + 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), 'icon' => 'bi bi-trash', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_DELETE') ); @@ -1108,7 +1108,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $itemsData->readItemData($item['ini_uuid']); $rowValues = array(); $rowValues['item_uuid'] = $item['ini_uuid']; - $strikethrough = $item['ini_former']; + $strikethrough = $item['ini_retired']; $columnNumber = 1; foreach ($itemsData->getItemFields() as $itemField) { @@ -1179,16 +1179,16 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database')) && !$item['ini_former'])) { + if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database')) && !$item['ini_retired'])) { // Add edit action $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), 'icon' => 'bi bi-pencil-square', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); // Add lend action - if (!$item['ini_former'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { + if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { $itemLended = false; @@ -1215,20 +1215,20 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter ); } - if ($item['ini_former']) { - $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_UNDO_FORMER_DESC', array('SYS_INVENTORY_ITEM_UNDO_FORMER_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_UNDO_FORMER_CONFIRM'); - // Add undo former action + if ($item['ini_retired']) { + $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); + // Add reinstate action $rowValues['actions'][] = array( - 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_undo_former', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', + 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', 'dataMessage' => $dataMessage, 'icon' => 'bi bi-eye', - 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_UNDO_FORMER') + 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE') ); } if ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (!$item['ini_former']) { - // Add make former action + if (!$item['ini_retired']) { + // Add retire action $rowValues['actions'][] = array( 'popup' => true, 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_keeper_explain_msg', 'item_uuid' => $item['ini_uuid'])), @@ -1238,10 +1238,10 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } } else { - // Add delete/make former action + // Add delete/retire action $rowValues['actions'][] = array( 'popup' => true, - 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_former' => $item['ini_former'])), + 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), 'icon' => 'bi bi-trash', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_DELETE') ); From db52de36dc62d16603d80b386bdaeb80c3a2539a Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Thu, 31 Jul 2025 17:50:42 +0200 Subject: [PATCH 02/25] feat: replace lend item logic with borrow item logic and redesign database table --- demo_data/db.sql | 2 +- install/db_scripts/db.sql | 25 ++- install/db_scripts/preferences.php | 6 +- install/db_scripts/update_5_0.xml | 26 +-- languages/en.xml | 16 +- modules/inventory.php | 14 +- modules/profile/profile.php | 6 +- src/Changelog/Entity/LogChanges.php | 4 +- src/Changelog/Service/ChangelogService.php | 22 +- .../Service/UpdateStepsCode.php | 4 +- src/Inventory/Entity/ItemBorrowData.php | 80 ++++++++ src/Inventory/Entity/ItemField.php | 5 - src/Inventory/Entity/ItemLendData.php | 160 --------------- src/Inventory/Service/ImportService.php | 16 +- src/Inventory/ValueObjects/ItemsData.php | 101 +++++---- src/Organizations/Entity/Organization.php | 8 +- src/UI/Presenter/InventoryFieldsPresenter.php | 8 +- src/UI/Presenter/InventoryImportPresenter.php | 8 +- src/UI/Presenter/InventoryItemPresenter.php | 193 +++++++++--------- src/UI/Presenter/InventoryPresenter.php | 48 ++--- src/UI/Presenter/PreferencesPresenter.php | 20 +- system/bootstrap/constants.php | 2 +- ...end.tpl => inventory.item.edit.borrow.tpl} | 0 .../preferences/preferences.inventory.tpl | 2 +- 24 files changed, 351 insertions(+), 425 deletions(-) create mode 100644 src/Inventory/Entity/ItemBorrowData.php delete mode 100644 src/Inventory/Entity/ItemLendData.php rename themes/simple/templates/modules/{inventory.item.edit.lend.tpl => inventory.item.edit.borrow.tpl} (100%) diff --git a/demo_data/db.sql b/demo_data/db.sql index 8e0a991212..3a17f076c0 100644 --- a/demo_data/db.sql +++ b/demo_data/db.sql @@ -63,7 +63,7 @@ DROP TABLE IF EXISTS %PREFIX%_inventory_fields CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_field_select_options CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_item_data CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_items CASCADE; -DROP TABLE IF EXISTS %PREFIX%_inventory_item_lend_data CASCADE; +DROP TABLE IF EXISTS %PREFIX%_inventory_item_borrow_data CASCADE; diff --git a/install/db_scripts/db.sql b/install/db_scripts/db.sql index c1d8ea0765..ff34b45c1d 100644 --- a/install/db_scripts/db.sql +++ b/install/db_scripts/db.sql @@ -54,7 +54,7 @@ DROP TABLE IF EXISTS %PREFIX%_menu CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_fields CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_field_select_options CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_item_data CASCADE; -DROP TABLE IF EXISTS %PREFIX%_inventory_item_lend_data CASCADE; +DROP TABLE IF EXISTS %PREFIX%_inventory_item_borrow_data CASCADE; DROP TABLE IF EXISTS %PREFIX%_inventory_items CASCADE; DROP TABLE IF EXISTS %PREFIX%_saml_clients CASCADE; DROP TABLE IF EXISTS %PREFIX%_sso_keys CASCADE; @@ -1060,21 +1060,22 @@ COLLATE = utf8_unicode_ci; CREATE UNIQUE INDEX %PREFIX%_idx_ind_inf_ini_id ON %PREFIX%_inventory_item_data (ind_inf_id, ind_ini_id); /*==============================================================*/ -/* Table: adm_inventory_item_lend_data */ +/* Table: adm_inventory_item_borrow_data */ /*==============================================================*/ -CREATE TABLE %PREFIX%_inventory_item_lend_data +CREATE TABLE %PREFIX%_inventory_item_borrow_data ( - inl_id integer unsigned NOT NULL AUTO_INCREMENT, - inl_inf_id integer unsigned NOT NULL, - inl_ini_id integer unsigned NOT NULL, - inl_value varchar(4000), - PRIMARY KEY (inl_id) + inb_id integer unsigned NOT NULL AUTO_INCREMENT, + inb_ini_id integer unsigned NOT NULL, + inb_last_receiver varchar(255) NULL DEFAULT NULL, + inb_borrow_date varchar(100) NULL DEFAULT NULL, + inb_return_date varchar(100) NULL DEFAULT NULL, + PRIMARY KEY (inb_id) ) ENGINE = InnoDB DEFAULT character SET = utf8 COLLATE = utf8_unicode_ci; -CREATE UNIQUE INDEX %PREFIX%_idx_inl_inf_ini_id ON %PREFIX%_inventory_item_lend_data (inl_inf_id, inl_ini_id); +CREATE UNIQUE INDEX %PREFIX%_idx_inb_ini_id ON %PREFIX%_inventory_item_borrow_data (inb_ini_id); /*==============================================================*/ /* Table: adm_inventory_items */ @@ -1356,10 +1357,8 @@ ALTER TABLE %PREFIX%_inventory_item_data ADD CONSTRAINT %PREFIX%_fk_ind_inf FOREIGN KEY (ind_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ind_ini FOREIGN KEY (ind_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; -ALTER TABLE %PREFIX%_inventory_item_lend_data - ADD CONSTRAINT %PREFIX%_fk_inl_inf FOREIGN KEY (inl_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, - ADD CONSTRAINT %PREFIX%_fk_inl_ini FOREIGN KEY (inl_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; - +ALTER TABLE %PREFIX%_inventory_item_borrow_data + ADD CONSTRAINT %PREFIX%_fk_inb_ini FOREIGN KEY (inb_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; ALTER TABLE %PREFIX%_inventory_items ADD CONSTRAINT %PREFIX%_fk_ini_cat FOREIGN KEY (ini_cat_id) REFERENCES %PREFIX%_categories (cat_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ini_usr_create FOREIGN KEY (ini_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, diff --git a/install/db_scripts/preferences.php b/install/db_scripts/preferences.php index d95fb65dd3..f09ebf891e 100644 --- a/install/db_scripts/preferences.php +++ b/install/db_scripts/preferences.php @@ -87,7 +87,7 @@ 'changelog_table_inventory_field_select_options' => '0', 'changelog_table_inventory_items' => '0', 'changelog_table_inventory_item_data' => '0', - 'changelog_table_inventory_item_lend_data' => '0', + 'changelog_table_inventory_item_borrow_data' => '0', 'changelog_table_saml_clients' => '0', 'changelog_table_oidc_clients' => '0', 'changelog_table_others' => '0', @@ -153,12 +153,12 @@ 'inventory_show_obsolete_select_field_options' => '1', 'inventory_system_field_names_editable' => '0', 'inventory_allow_keeper_edit' => '0', - 'inventory_allowed_keeper_edit_fields' => 'IN_INVENTORY,LAST_RECEIVER,RECEIVED_ON,RECEIVED_BACK_ON', + 'inventory_allowed_keeper_edit_fields' => 'IN_INVENTORY,LAST_RECEIVER,BORROW_DATE,RETURN_DATE', 'inventory_current_user_default_keeper' => '0', 'inventory_allow_negative_numbers' => '1', 'inventory_decimal_places' => '1', 'inventory_field_date_time_format' => 'date', - 'inventory_items_disable_lending' => '0', + 'inventory_items_disable_borrowing' => '0', 'inventory_profile_view_enabled' => '1', 'inventory_profile_view' => 'LAST_RECEIVER', 'inventory_export_filename' => $GLOBALS['gL10n']->get('SYS_INVENTORY'), diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index e185ec6037..cb0e11b5de 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -444,20 +444,20 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ALTER TABLE %PREFIX%_inventory_item_data ADD CONSTRAINT %PREFIX%_fk_ind_inf FOREIGN KEY (ind_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ind_ini FOREIGN KEY (ind_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; - CREATE TABLE IF NOT EXISTS %PREFIX%_inventory_item_lend_data ( - inl_id integer unsigned NOT NULL AUTO_INCREMENT, - inl_inf_id integer unsigned NOT NULL, - inl_ini_id integer unsigned NOT NULL, - inl_value varchar(4000), - PRIMARY KEY (inl_id) + CREATE TABLE IF NOT EXISTS %PREFIX%_inventory_item_borrow_data ( + inb_id integer unsigned NOT NULL AUTO_INCREMENT, + inb_ini_id integer unsigned NOT NULL, + inb_last_receiver varchar(255) NULL DEFAULT NULL, + inb_borrow_date varchar(255) NULL DEFAULT NULL, + inb_return_date varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (inb_id) ) ENGINE = InnoDB DEFAULT character SET = utf8 COLLATE = utf8_unicode_ci; - CREATE UNIQUE INDEX %PREFIX%_idx_inl_inf_ini_id ON %PREFIX%_inventory_item_lend_data (inl_inf_id, inl_ini_id); - ALTER TABLE %PREFIX%_inventory_item_lend_data - ADD CONSTRAINT %PREFIX%_fk_inl_inf FOREIGN KEY (inl_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, - ADD CONSTRAINT %PREFIX%_fk_inl_ini FOREIGN KEY (inl_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; + CREATE UNIQUE INDEX %PREFIX%_idx_inb_ini_id ON %PREFIX%_inventory_item_borrow_data (inb_ini_id); + ALTER TABLE %PREFIX%_inventory_item_borrow_data + ADD CONSTRAINT %PREFIX%_fk_inb_ini FOREIGN KEY (inb_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; CREATE TABLE IF NOT EXISTS %PREFIX%_inventory_items ( ini_id integer unsigned NOT NULL AUTO_INCREMENT, ini_uuid varchar(36) NOT NULL, @@ -488,8 +488,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N UpdateStepsCode::updateStep50InventoryCategories UPDATE %PREFIX%_preferences pr1 SET prf_value = 0 WHERE prf_name = 'inventory_module_enabled' - UpdateStepsCode::updateStep50AddInventoryFields - CREATE TABLE IF NOT EXISTS %PREFIX%_inventory_field_select_options ( + CREATE TABLE IF NOT EXISTS %PREFIX%_inventory_field_select_options ( ifo_id integer unsigned NOT NULL AUTO_INCREMENT, ifo_inf_id integer unsigned NOT NULL, ifo_value varchar(255) NOT NULL, @@ -500,7 +499,8 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ENGINE = InnoDB DEFAULT character SET = utf8 COLLATE = utf8_unicode_ci; - ALTER TABLE %PREFIX%_inventory_field_select_options + ALTER TABLE %PREFIX%_inventory_field_select_options ADD CONSTRAINT %PREFIX%_fk_ifo_inf FOREIGN KEY (ifo_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE CASCADE ON UPDATE RESTRICT; + UpdateStepsCode::updateStep50AddInventoryFields stop diff --git a/languages/en.xml b/languages/en.xml index 4fe9751956..0b6d14ac8e 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -783,6 +783,8 @@ When this option is enabled, the current date in the format YYYY-MM-DD will be prefixed to the export file’s name. (Default: no) Allow negative numbers If this option is enabled, negative numbers can also be entered in fields of type "Number" or "Decimal number". Otherwise, only positive numbers are allowed. (Default: yes) + Borrowing date + The borrowing date of the item to the last recipient The category of an item Settings for copying a item: Date representation @@ -813,8 +815,8 @@ Item successfully deleted Change the item Item successfully changed - Lend the item - Item lend data + Borrow the item + Item borrowing data Reinstate the item Do you really want to reinstate the item? Item successfully reinstated @@ -824,8 +826,8 @@ Item retired? Return the item Items - Disable borrowing functionality - When this option is enabled, items can no longer be borrowed. Existing borrowing records will still be saved, but will no longer be displayed. (Default: no) + Disable borrowing functionality + When this option is enabled, items can no longer be borrowed. Existing borrowing records will still be saved, but will no longer be displayed. (Default: no) Change items The values of the item fields can be changed here.\n\nThe displayed values will be applied to ALL items! Number of items per page @@ -868,10 +870,8 @@ Property fields to display in the profile view The table shows the items managed by you: The table shows the items loaned to you: - Received back on - The date the item was returned to the keeper - Borrowed on - The borrowing date of the item to the last recipient + Return date + The date the item was returned to the keeper Delete selected items If you select #VAR1_BOLD#, the items along with their associated records will be irrevocably removed from the database, and it will not be possible to view the data of these items later. The selected items have been deleted. diff --git a/modules/inventory.php b/modules/inventory.php index 9f1cf4a93a..839af43e4e 100644 --- a/modules/inventory.php +++ b/modules/inventory.php @@ -39,7 +39,7 @@ require(__DIR__ . '/../system/login_valid.php'); // Initialize and check the parameters - $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_lend', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_retire', 'item_reinstate', 'item_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); + $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_borrow', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_retire', 'item_reinstate', 'item_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); $getinfUUID = admFuncVariableIsValid($_GET, 'uuid', 'uuid'); $getOptionID = admFuncVariableIsValid($_GET, 'option_id', 'int', array('defaultValue' => 0)); $getFieldName = admFuncVariableIsValid($_GET, 'field_name', 'string', array('defaultValue' => "", 'directOutput' => true)); @@ -58,7 +58,7 @@ return preg_replace('/adm_inventory_item_/', '', $uuid); }, $getItemUUIDs); } - $getLended = admFuncVariableIsValid($_GET, 'item_lended', 'bool', array('defaultValue' => false)); + $getBorrowed = admFuncVariableIsValid($_GET, 'item_borrowed', 'bool', array('defaultValue' => false)); // check if module is active if ($gSettingsManager->getInt('inventory_module_enabled') === 0) { @@ -216,18 +216,18 @@ $item->show(); break; - case 'item_edit_lend': + case 'item_edit_borrow': // set headline of the script - if ($getLended) { + if ($getBorrowed) { $headline = $gL10n->get('SYS_INVENTORY_ITEM_RETURN'); } else { - $headline = $gL10n->get('SYS_INVENTORY_ITEM_LEND'); + $headline = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); } $gNavigation->addUrl(CURRENT_URL, $headline); - $item = new InventoryItemPresenter('adm_item_edit_lend'); + $item = new InventoryItemPresenter('adm_item_edit_borrow'); $item->setHeadline($headline); - $item->createEditLendForm($getiniUUID); + $item->createEditBorrowForm($getiniUUID); $item->show(); break; diff --git a/modules/profile/profile.php b/modules/profile/profile.php index ec5afc3d18..07f82853d6 100644 --- a/modules/profile/profile.php +++ b/modules/profile/profile.php @@ -408,11 +408,11 @@ function formSubmitEvent(rolesAreaId = "") { // Determine creation mode based on available items $creationMode = 'none'; - if (!empty($itemsKeeper->getItems()) && (empty($itemsReceiver->getItems()) || $gSettingsManager->GetBool('inventory_items_disable_lending'))) { + if (!empty($itemsKeeper->getItems()) && (empty($itemsReceiver->getItems()) || $gSettingsManager->GetBool('inventory_items_disable_borrowing'))) { $creationMode = 'keeper'; - } elseif (empty($itemsKeeper->getItems()) && (!empty($itemsReceiver->getItems()) && !$gSettingsManager->GetBool('inventory_items_disable_lending'))) { + } elseif (empty($itemsKeeper->getItems()) && (!empty($itemsReceiver->getItems()) && !$gSettingsManager->GetBool('inventory_items_disable_borrowing'))) { $creationMode = 'receiver'; - } elseif (!empty($itemsKeeper->getItems()) && (!empty($itemsReceiver->getItems()) && !$gSettingsManager->GetBool('inventory_items_disable_lending'))) { + } elseif (!empty($itemsKeeper->getItems()) && (!empty($itemsReceiver->getItems()) && !$gSettingsManager->GetBool('inventory_items_disable_borrowing'))) { $creationMode = 'both'; } diff --git a/src/Changelog/Entity/LogChanges.php b/src/Changelog/Entity/LogChanges.php index c18807bf66..f8a3453ccb 100644 --- a/src/Changelog/Entity/LogChanges.php +++ b/src/Changelog/Entity/LogChanges.php @@ -135,8 +135,8 @@ private function connectReferencedTable(string $table) case 'inventory_items': $this->connectAdditionalTable(TBL_INVENTORY_ITEMS, 'ini_id', 'log_record_id'); break; - case 'inventory_item_lend_data': - $this->connectAdditionalTable(TBL_INVENTORY_ITEM_LEND_DATA, 'inl_id', 'log_record_id'); + case 'inventory_item_borrow_data': + $this->connectAdditionalTable(TBL_INVENTORY_ITEM_BORROW_DATA, 'inb_id', 'log_record_id'); break; case 'inventory_item_data': $this->connectAdditionalTable(TBL_INVENTORY_ITEM_DATA, 'ind_id', 'log_record_id'); diff --git a/src/Changelog/Service/ChangelogService.php b/src/Changelog/Service/ChangelogService.php index 0a64dc194c..bc0ab2a15d 100644 --- a/src/Changelog/Service/ChangelogService.php +++ b/src/Changelog/Service/ChangelogService.php @@ -17,8 +17,6 @@ use Admidio\Forum\Entity\Post; use Admidio\Inventory\Entity\ItemField; use Admidio\Inventory\Entity\Item; -use Admidio\Inventory\Entity\ItemData; -use Admidio\Inventory\Entity\ItemLendData; use Admidio\Roles\Entity\ListColumns; use Admidio\Roles\Entity\ListConfiguration; @@ -192,7 +190,7 @@ public static function getTableLabel(mixed $table = null): array|string { 'inventory_field_select_options' => 'SYS_INVENTORY_ITEMFIELD_SELECT_OPTIONS', 'inventory_items' => 'SYS_INVENTORY_ITEMS', 'inventory_item_data' => 'SYS_INVENTORY_ITEM_DATA', - 'inventory_item_lend_data' => 'SYS_INVENTORY_ITEM_LEND_DATA', + 'inventory_item_borrow_data' => 'SYS_INVENTORY_ITEM_BORROW_DATA', 'organizations' => 'SYS_ORGANIZATION', 'menu' => 'SYS_MENU_ITEM', @@ -325,7 +323,7 @@ public static function getObjectForTable(string $module): Entity | null { case 'inventory_fields': return new ItemField($gDb); case 'inventory_item_data': - case 'inventory_item_lend_data': + case 'inventory_item_borrow_data': case 'inventory_items': return new Item($gDb); default: @@ -533,17 +531,13 @@ public static function getFieldTranslations(): array 'ind_value_icon' => array('name' => 'SYS_VALUE', 'type' => 'ICON'), 'ind_value_usr' => array('name' => 'SYS_VALUE', 'type' => 'USER'), 'ind_value' => 'SYS_VALUE', - 'inl_value_bool' => array('name' => 'SYS_VALUE', 'type' => 'BOOL'), - 'inl_value_date' => array('name' => 'SYS_VALUE', 'type' => 'DATE'), - 'inl_value_mail' => array('name' => 'SYS_VALUE', 'type' => 'EMAIL'), - 'inl_value_url' => array('name' => 'SYS_VALUE', 'type' => 'URL'), - 'inl_value_icon' => array('name' => 'SYS_VALUE', 'type' => 'ICON'), - 'inl_value_usr' => array('name' => 'SYS_VALUE', 'type' => 'USER'), - 'inl_value' => 'SYS_VALUE', + 'inb_last_receiver' => array('name' => 'SYS_INVENTORY_LAST_RECEIVER', 'type' => 'USER'), + 'inb_borrow_date' => array('name' => 'SYS_INVENTORY_BORROW_DATE', 'type' => 'DATE'), + 'inb_return_date' => array('name' => 'SYS_INVENTORY_RETURN_DATE', 'type' => 'DATE'), 'ifo_value' => 'SYS_VALUE', 'ifo_inf_id' => 'SYS_INVENTORY_ITEMFIELD', 'ifo_sequence' => 'SYS_ORDER', - 'ifo_obsolete' => array('name' => 'SYS_DELETED', 'type' => 'BOOL'), + 'ifo_obsolete' => array('name' => 'SYS_DELETED', 'type' => 'BOOL'), 'lnk_name' => 'SYS_LINK_NAME', 'lnk_description' => 'SYS_DESCRIPTION', @@ -732,8 +726,8 @@ public static function createLink(string $text, string $module, int|string $id, case 'inventory_item_data' : // Fall through case 'inventory_items' : $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $uuid)); break; - case 'inventory_item_lend_data' : - $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/inventory.php', array('mode' => 'item_edit_lend', 'item_uuid' => $uuid)); break; + case 'inventory_item_borrow_data' : + $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/inventory.php', array('mode' => 'item_edit_borrow', 'item_uuid' => $uuid)); break; case 'links' : $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/links/links_new.php', array('link_uuid' => $uuid)); break; case 'lists' : diff --git a/src/InstallationUpdate/Service/UpdateStepsCode.php b/src/InstallationUpdate/Service/UpdateStepsCode.php index 91c13513a5..4b0e1a508e 100644 --- a/src/InstallationUpdate/Service/UpdateStepsCode.php +++ b/src/InstallationUpdate/Service/UpdateStepsCode.php @@ -91,8 +91,8 @@ public static function updateStep50AddInventoryFields() array('inf_type' => 'TEXT', 'inf_name_intern' => 'KEEPER', 'inf_name' => 'SYS_INVENTORY_KEEPER', 'inf_description' => 'SYS_INVENTORY_KEEPER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 2), array('inf_type' => 'CHECKBOX', 'inf_name_intern' => 'IN_INVENTORY', 'inf_name' => 'SYS_INVENTORY_IN_INVENTORY', 'inf_description' => 'SYS_INVENTORY_IN_INVENTORY_DESC', 'inf_required_input' => 0, 'inf_sequence' => 3), array('inf_type' => 'TEXT', 'inf_name_intern' => 'LAST_RECEIVER', 'inf_name' => 'SYS_INVENTORY_LAST_RECEIVER', 'inf_description' => 'SYS_INVENTORY_LAST_RECEIVER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 4), - array('inf_type' => 'DATE', 'inf_name_intern' => 'RECEIVED_ON', 'inf_name' => 'SYS_INVENTORY_RECEIVED_ON', 'inf_description' => 'SYS_INVENTORY_RECEIVED_ON_DESC', 'inf_required_input' => 0, 'inf_sequence' => 5), - array('inf_type' => 'DATE', 'inf_name_intern' => 'RECEIVED_BACK_ON', 'inf_name' => 'SYS_INVENTORY_RECEIVED_BACK_ON', 'inf_description' => 'SYS_INVENTORY_RECEIVED_BACK_ON_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6) + array('inf_type' => 'DATE', 'inf_name_intern' => 'BORROW_DATE', 'inf_name' => 'SYS_INVENTORY_BORROW_DATE', 'inf_description' => 'SYS_INVENTORY_BORROW_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 5), + array('inf_type' => 'DATE', 'inf_name_intern' => 'RETURN_DATE', 'inf_name' => 'SYS_INVENTORY_RETURN_DATE', 'inf_description' => 'SYS_INVENTORY_RETURN_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6) ); $sql = 'SELECT org_id, org_shortname FROM ' . TBL_ORGANIZATIONS; diff --git a/src/Inventory/Entity/ItemBorrowData.php b/src/Inventory/Entity/ItemBorrowData.php new file mode 100644 index 0000000000..7a034cc76f --- /dev/null +++ b/src/Inventory/Entity/ItemBorrowData.php @@ -0,0 +1,80 @@ +mItemsData = clone $itemsData; // create explicit a copy of the object (param is in PHP5 a reference) + } else { + $this->mItemsData = new ItemsData($database, $gCurrentOrgId); + } + + parent::__construct($database, TBL_INVENTORY_ITEM_BORROW_DATA, 'inb', $id); + } + + public function updateRecordId(int $recordId) : void + { + if ($recordId !== 0) { + $this->setValue('inb_id', $recordId); + $this->newRecord = false; + $this->insertRecord = false; + } + } + + /** + * Adjust the changelog entry for this db record: Add the first forum post as a related object + * @param LogChanges $logEntry The log entry to adjust + * @return void + * @throws Exception + */ + protected function adjustLogEntry(LogChanges $logEntry): void + { + global $gDb; + + $itemID = $this->getValue('inb_ini_id'); + $item = new Item($gDb, $this->mItemsData, $itemID); + $itemUUID = $item->getValue('ini_uuid'); + $itemName = $item->readableName(); + + $logEntry->setValue('log_record_name', $itemName); + $logEntry->setValue('log_record_uuid', $itemUUID); + } +} diff --git a/src/Inventory/Entity/ItemField.php b/src/Inventory/Entity/ItemField.php index 558b146234..b4f881e685 100644 --- a/src/Inventory/Entity/ItemField.php +++ b/src/Inventory/Entity/ItemField.php @@ -81,11 +81,6 @@ public function delete(): bool WHERE ifo_inf_id = ? -- $infId'; $this->db->queryPrepared($sql, array($infId)); - // delete all data of this field in the item lend data table - $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' - WHERE inl_inf_id = ? -- $infId'; - $this->db->queryPrepared($sql, array($infId)); - $return = parent::delete(); $this->db->endTransaction(); diff --git a/src/Inventory/Entity/ItemLendData.php b/src/Inventory/Entity/ItemLendData.php deleted file mode 100644 index ba80269f96..0000000000 --- a/src/Inventory/Entity/ItemLendData.php +++ /dev/null @@ -1,160 +0,0 @@ -mItemsData = clone $itemsData; // create explicit a copy of the object (param is in PHP5 a reference) - } else { - $this->mItemsData = new ItemsData($database, $gCurrentOrgId); - } - - $this->connectAdditionalTable(TBL_INVENTORY_FIELDS, 'inf_id', 'inl_inf_id'); - parent::__construct($database, TBL_INVENTORY_ITEM_LEND_DATA, 'inl', $id); - } - - /** - * Since creation means setting value from NULL to something, deletion mean setting the field to empty, - * we need one generic change log function that is called on creation, deletion and modification. - * - * The log entries are: record ID for inl_id, but uuid and link point to User id. - * log_field is the inf_id and log_field_name is the fields external name. - * - * @param string $oldval previous value before the change (can be null) - * @param string $newval new value after the change (can be null) - * @return true returns **true** if no error occurred - */ - protected function logLendChange(?string $oldval = null, ?string $newval = null) : bool { - global $gDb, $gProfileFields; - - if ($oldval === $newval) { - // No change, nothing to log - return true; - } - - $table = str_replace(TABLE_PREFIX . '_', '', $this->tableName); - - $itemID = $this->getValue('inl_ini_id'); - $item = new Item($gDb, $this->mItemsData, $itemID); - $itemUUID = $item->getValue('ini_uuid'); - - $id = $this->dbColumns[$this->keyColumnName]; - $field = $this->getValue('inl_inf_id'); - $fieldName = 'inl_value'; - $objectName = $this->mItemsData->getPropertyById($field, 'inf_name', 'database'); - $fieldNameIntern = $this->mItemsData->getPropertyById($field, 'inf_name_intern', 'database'); - $infType = $this->mItemsData->getPropertyById($field, 'inf_type'); - - if ($infType === 'TEXT') { - if ($fieldNameIntern === 'KEEPER') { - $fieldName = $fieldName . '_usr'; - } - elseif ($fieldNameIntern === 'LAST_RECEIVER') { - $user = new User($this->db, $gProfileFields); - if (is_numeric($oldval) && is_numeric($newval)) { - $foundOld = $user->readDataById($oldval); - $foundNew = $user->readDataById($newval); - if ($foundOld && $foundNew) { - $fieldName = $fieldName . '_usr'; - } - } - elseif (is_numeric($oldval)) { - if ($user->readDataById($oldval)) { - $oldval = '' . $user->getValue('LAST_NAME') . ', ' . $user->getValue('FIRST_NAME') . ''; - } - } elseif (is_numeric($newval)) { - if ($user->readDataById($newval)) { - $newval = '' . $user->getValue('LAST_NAME') . ', ' . $user->getValue('FIRST_NAME') . ''; - } - } - } - } - elseif ($infType === 'DATE') { - $fieldName = $fieldName . '_date'; - } - - $logEntry = new LogChanges($this->db, $table); - $logEntry->setLogModification($table, $id, $itemUUID, $objectName, $field, $fieldName, $oldval, $newval); - /* $logEntry->setLogRelated($itemUUID, $itemName); */ - $logEntry->setLogLinkID($itemID); - return $logEntry->save(); - } - - - /** - * Logs creation of the DB record -> For user fields, no need to log anything as - * the actual value change from NULL to something will be logged as a modification - * immediately after creation, anyway. - * - * @return true Returns **true** if no error occurred - * @throws Exception - */ - public function logCreation(): bool { return true; } - - /** - * Logs deletion of the DB record - * Deletion actually means setting the user field to an empty value, so log a change to empty instead of deletion! - * - * @return true Returns **true** if no error occurred - */ - public function logDeletion(): bool - { - $oldval = $this->columnsInfos['inl_value']['previousValue']; - return $this->logLendChange($oldval, null); - } - - - /** - * Logs all modifications of the DB record - * @param array $logChanges Array of all changes, generated by the save method - * @return true Returns **true** if no error occurred - * @throws Exception - */ - public function logModifications(array $logChanges): bool - { - if ($logChanges['inl_value']) { - return $this->logLendChange($logChanges['inl_value']['oldValue'], $logChanges['inl_value']['newValue']); - } else { - // Nothing to log at all! - return true; - } - } -} diff --git a/src/Inventory/Service/ImportService.php b/src/Inventory/Service/ImportService.php index be0c0b1432..32033e8848 100644 --- a/src/Inventory/Service/ImportService.php +++ b/src/Inventory/Service/ImportService.php @@ -221,8 +221,8 @@ public function importItems(): array foreach ($items->getItemData() as $key => $itemData) { $itemValue = $itemData->getValue('ind_value'); if ($itemData->getValue('inf_name_intern') === 'KEEPER' || $itemData->getValue('inf_name_intern') === 'LAST_RECEIVER' || - $itemData->getValue('inf_name_intern') === 'IN_INVENTORY' || $itemData->getValue('inf_name_intern') === 'RECEIVED_ON' || - $itemData->getValue('inf_name_intern') === 'RECEIVED_BACK_ON') { + $itemData->getValue('inf_name_intern') === 'IN_INVENTORY' || $itemData->getValue('inf_name_intern') === 'BORROW_DATE' || + $itemData->getValue('inf_name_intern') === 'RETURN_DATE') { continue; } @@ -254,15 +254,15 @@ public function importItems(): array // get all values of the item fields $importedItemData = array(); - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrowing fields + $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); foreach ($assignedFieldColumn as $row => $values) { foreach ($items->getItemFields() as $fields){ $infId = $fields->getValue('inf_id'); $imfNameIntern = $fields->getValue('inf_name_intern'); - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($imfNameIntern, $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($imfNameIntern, $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } if (isset($values[$infId])) { @@ -343,7 +343,7 @@ public function importItems(): array } } - elseif($imfNameIntern === 'RECEIVED_ON' || $imfNameIntern === 'RECEIVED_BACK_ON') { + elseif($imfNameIntern === 'BORROWING_DATE' || $imfNameIntern === 'RETURN_DATE') { $val = $values[$infId]; if ($val !== '') { // date must be formatted @@ -432,7 +432,7 @@ public function importItems(): array private function compareArrays(array $array1, array $array2) : bool { $array1 = array_filter($array1, function($key) { - return $key !== 'KEEPER' && $key !== 'LAST_RECEIVER' && $key !== 'IN_INVENTORY' && $key !== 'RECEIVED_ON' && $key !== 'RECEIVED_BACK_ON'; + return $key !== 'KEEPER' && $key !== 'LAST_RECEIVER' && $key !== 'IN_INVENTORY' && $key !== 'BORROW_DATE' && $key !== 'RETURN_DATE'; }, ARRAY_FILTER_USE_KEY); foreach ($array1 as $value) { diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index 953a2ae1ce..091bc29c34 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -13,7 +13,7 @@ use Admidio\Inventory\Entity\Item; use Admidio\Inventory\Entity\ItemData; use Admidio\Inventory\Entity\ItemField; -use Admidio\Inventory\Entity\ItemLendData; +use Admidio\Inventory\Entity\ItemBorrowData; use Admidio\Categories\Entity\Category; // PHP namespaces @@ -42,7 +42,7 @@ class ItemsData private bool $mItemImported = false; ///< flag if a item was imported private bool $showRetiredItems = true; ///< if true, than retired items will be showed private int $organizationId = -1; ///< ID of the organization for which the item field structure should be read - private array $lendFieldNames = array('LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); ///< array with the internal field names of the lend fields + private array $borrowFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); ///< array with the internal field names of the borrow fields /** * @var Database An object of the class Database for communication with the database @@ -163,7 +163,7 @@ public function readItemFields($orderBy = 'inf_id'): void /** * Reads the item data of all item fields out of database table @b adm_inventory_manager_data - * and @b adm_inventory_manager_items_lend + * and @b adm_inventory_manager_items_borrow * and adds an object for each field data to the @b mItemData array. * If profile fields structure wasn't read, this will be done before. * @@ -201,19 +201,25 @@ public function readItemData(string $itemUUID = ''): void $this->mItemData[$row['ind_inf_id']]->setArray($row); } - // read all item lend data - $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' - INNER JOIN ' . TBL_INVENTORY_FIELDS . ' - ON inf_id = inl_inf_id - WHERE inl_ini_id = ?;'; - $itemLendStatement = $this->mDb->queryPrepared($sql, array($itemId)); - - while ($row = $itemLendStatement->fetch()) { - if (!array_key_exists($row['inl_inf_id'], $this->mItemData)) { - $this->mItemData[$row['inl_inf_id']] = new ItemLendData($this->mDb, $this, $row['inl_inf_id']); + // read all item borrow data + $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' + WHERE inb_ini_id = ?;'; + $itemBorrowStatement = $this->mDb->queryPrepared($sql, array($itemId)); + + while ($row = $itemBorrowStatement->fetch()) { + foreach ($this->getItemFields() as $itemField) { + $itemBorrowData = new ItemBorrowData($this->mDb, $this, $row['inb_ini_id']); + $fieldNameIntern = $itemField->getValue('inf_name_intern'); + $fieldId = $itemField->getValue('inf_id'); + if (in_array($fieldNameIntern, $this->borrowFieldNames)) { + if (!array_key_exists($fieldId, $this->mItemData)) { + $this->mItemData[$fieldId] = $itemBorrowData; + } + $this->mItemData[$fieldId]->setArray($row); + } } - $this->mItemData[$row['inl_inf_id']]->setArray($row); - } } else { + } + } else { $this->mItemCreated = true; } } @@ -290,16 +296,13 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); } - // now read the item lend data for each item - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' - INNER JOIN ' . TBL_INVENTORY_FIELDS . ' - ON inf_id = inl_inf_id - ' . $sqlImfIds . ' + // now read the item borrow data for each item + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' INNER JOIN ' . TBL_INVENTORY_ITEMS . ' - ON ini_id = inl_ini_id + ON ini_id = inb_ini_id WHERE (ini_org_id IS NULL OR ini_org_id = ?) - AND inl_value = ? + AND inb_last_receiver = ? ' . $sqlWhereCondition . ';'; $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); // check if a item already exists in the items array @@ -616,12 +619,12 @@ public function getValue($fieldNameIntern, $format = '') } } elseif (array_key_exists($this->mItemFields[$fieldNameIntern]->getValue('inf_id'), $this->mItemData)) { - $prefix = 'ind'; - if ($this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')] instanceof ItemLendData) { - // if field is a lend field then use 'inl_' as prefix - $prefix = 'inl'; + if ($this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')] instanceof ItemBorrowData) { + $value = $this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')]->getValue('inb_' . strtolower($fieldNameIntern), $format); + } else { + $value = $this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')]->getValue('ind_value', $format); } - $value = $this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')]->getValue($prefix . '_value', $format); + if ($format === 'database') { return $value; @@ -744,14 +747,18 @@ public function setValue($fieldNameIntern, $newValue): bool $infId = $this->mItemFields[$fieldNameIntern]->getValue('inf_id'); $oldFieldValue = ''; // default prefix is 'ind_' for item data - // if field is a lend field then use 'inl_' as prefix + // if field is a borrow field then use 'inb_' as prefix $prefix = 'ind'; - if (in_array($fieldNameIntern, $this->lendFieldNames)) { - $prefix = 'inl'; + if (in_array($fieldNameIntern, $this->borrowFieldNames)) { + $prefix = 'inb'; } if (array_key_exists($infId, $this->mItemData)) { - $oldFieldValue = $this->mItemData[$infId]->getValue($prefix .'_value'); + if ($this->mItemData[$infId] instanceof ItemBorrowData) { + $oldFieldValue = $this->mItemData[$infId]->getValue($prefix . '_' . strtolower($fieldNameIntern)); + } else { + $oldFieldValue = $this->mItemData[$infId]->getValue($prefix . '_value'); + } } // check if new value only contains spaces @@ -796,16 +803,23 @@ public function setValue($fieldNameIntern, $newValue): bool // if item data object for this field does not exist then create it if (!array_key_exists($infId, $this->mItemData)) { - if (in_array($fieldNameIntern, $this->lendFieldNames)) { - $this->mItemData[$infId] = new ItemLendData($this->mDb, $this); + if (in_array($fieldNameIntern, $this->borrowFieldNames)) { + $this->mItemData[$infId] = new ItemBorrowData($this->mDb, $this); } else { $this->mItemData[$infId] = new ItemData($this->mDb, $this); + $this->mItemData[$infId]->setValue($prefix . '_inf_id', $infId); } - $this->mItemData[$infId]->setValue($prefix . '_inf_id', $infId); $this->mItemData[$infId]->setValue($prefix . '_ini_id', $this->mItemId); } - return $this->mItemData[$infId]->setValue($prefix . '_value', $newValue); + $ret = false; + if ($this->mItemData[$infId] instanceof ItemBorrowData) { + $ret = $this->mItemData[$infId]->setValue($prefix . '_' . strtolower($fieldNameIntern), $newValue); + } else { + $ret = $this->mItemData[$infId]->setValue($prefix . '_value', $newValue); + } + + return $ret; } /** @@ -862,8 +876,8 @@ public function deleteItem(): void // delete all item data $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_DATA . ' WHERE ind_ini_id = ?;'; $this->mDb->queryPrepared($sql, array($this->mItemId)); - // delete all item lend data - $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' WHERE inl_ini_id = ?;'; + // delete all item borrow data + $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' WHERE inb_ini_id = ?;'; $this->mDb->queryPrepared($sql, array($this->mItemId)); // delete item $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEMS . ' WHERE ini_id = ? AND (ini_org_id = ? OR ini_org_id IS NULL);'; @@ -913,7 +927,7 @@ public function saveItemData(): void { global $gCurrentUser; $this->mDb->startTransaction(); - + $inbId = 0; // used for item borrow data // safe item data foreach ($this->mItemData as $value) { if ($value->hasColumnsValueChanged()) { @@ -940,13 +954,12 @@ public function saveItemData(): void $value->save(); } } - elseif ($value instanceof ItemLendData) { - // if value exists and new value is empty then delete entry - if ($value->getValue('inl_id') > 0 && $value->getValue('inl_value') === '') { - $value->delete(); - } else { - $value->save(); + elseif ($value instanceof ItemBorrowData) { + if ($value->getValue('inb_id') === 0 && $inbId !== 0) { + $value->updateRecordId($inbId); } + $value->save(); + $inbId = $value->getValue('inb_id'); } } diff --git a/src/Organizations/Entity/Organization.php b/src/Organizations/Entity/Organization.php index a9525416a5..f8fda82019 100644 --- a/src/Organizations/Entity/Organization.php +++ b/src/Organizations/Entity/Organization.php @@ -231,8 +231,8 @@ public function createBasicData(int $userId) (?, ?, \'TEXT\', \'KEEPER\', \'SYS_INVENTORY_KEEPER\', \'SYS_INVENTORY_KEEPER_DESC\', 1, 0, 2, ?, ?, NULL, NULL), (?, ?, \'CHECKBOX\', \'IN_INVENTORY\', \'SYS_INVENTORY_IN_INVENTORY\', \'SYS_INVENTORY_IN_INVENTORY_DESC\', 1, 0, 3, ?, ?, NULL, NULL), (?, ?, \'TEXT\', \'LAST_RECEIVER\', \'SYS_INVENTORY_LAST_RECEIVER\', \'SYS_INVENTORY_LAST_RECEIVER_DESC\', 1, 0, 4, ?, ?, NULL, NULL), - (?, ?, \'DATE\', \'RECEIVED_ON\', \'SYS_INVENTORY_RECEIVED_ON\', \'SYS_INVENTORY_RECEIVED_ON_DESC\', 1, 0, 5, ?, ?, NULL, NULL), - (?, ?, \'DATE\', \'RECEIVED_BACK_ON\', \'SYS_INVENTORY_RECEIVED_BACK_ON\', \'SYS_INVENTORY_RECEIVED_BACK_ON_DESC\', 1, 0, 6, ?, ?, NULL, NULL); + (?, ?, \'DATE\', \'BORROW_DATE\', \'SYS_INVENTORY_BORROW_DATE\', \'SYS_INVENTORY_BORROW_DATE_DESC\', 1, 0, 5, ?, ?, NULL, NULL), + (?, ?, \'DATE\', \'RETURN_DATE\', \'SYS_INVENTORY_RETURN_DATE\', \'SYS_INVENTORY_RETURN_DATE_DESC\', 1, 0, 6, ?, ?, NULL, NULL); '; $queryParams = array( Uuid::uuid4(), $orgId, $systemUserId, DATETIME_NOW, @@ -672,8 +672,8 @@ public function delete(): bool $this->db->queryPrepared($sql, array($this->getValue('org_id'))); // delete all inventory item lend data - $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_LEND_DATA . ' - WHERE inl_ini_id IN ( + $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' + WHERE inb_ini_id IN ( SELECT ini.ini_id FROM (SELECT ini_id FROM ' . TBL_INVENTORY_ITEMS . ' diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index d0ecaff3be..241550ab68 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -246,12 +246,12 @@ public function createList() $templateItemFields = array(); $itemFieldCategoryID = -1; $prevItemFieldCategoryID = -1; - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrowing fields + $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); foreach ($items->getItemFields() as $itemField) { - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($itemField->getValue('inf_name_intern'), $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($itemField->getValue('inf_name_intern'), $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } $prevItemFieldCategoryID = $itemFieldCategoryID; $itemFieldCategoryID = ((bool)$itemField->getValue('inf_system')) ? 1 : 2; diff --git a/src/UI/Presenter/InventoryImportPresenter.php b/src/UI/Presenter/InventoryImportPresenter.php index 4c2c603af0..bd9209a585 100644 --- a/src/UI/Presenter/InventoryImportPresenter.php +++ b/src/UI/Presenter/InventoryImportPresenter.php @@ -268,12 +268,12 @@ public function createAssignFieldsForm(): void return !is_null($value); }); - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrowing fields + $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); $items = new ItemsData($gDb, $gCurrentOrgId); foreach ($items->getItemFields() as $itemField) { - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($itemField->getValue('inf_name_intern'), $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($itemField->getValue('inf_name_intern'), $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } $fieldDefaultValue = -1; diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index 61e1d30671..fb4cdbf6f4 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -12,6 +12,7 @@ use Admidio\UI\Presenter\FormPresenter; use Admidio\UI\Presenter\PagePresenter; use Admidio\Users\Entity\User; +use DateTime; use Ramsey\Uuid\Uuid; /** @@ -41,8 +42,8 @@ class InventoryItemPresenter extends PagePresenter public function createEditForm(string $itemUUID = '', bool $getCopy = false) { global $gCurrentSession, $gSettingsManager, $gCurrentUser, $gProfileFields, $gL10n, $gCurrentOrgId, $gDb; - //array with the internal field names of the lend fields not used in the edit form - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrow fields not used in the edit form + $borrowFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -78,8 +79,8 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) foreach ($items->getItemFields() as $itemField) { $helpId = ''; $infNameIntern = $itemField->getValue('inf_name_intern'); - // Skip lend fields that are not used in the edit form - if (in_array($itemField->getValue('inf_name_intern'), $lendFieldNames)) { + // Skip borrow fields that are not used in the edit form + if (in_array($itemField->getValue('inf_name_intern'), $borrowFieldNames)) { if ($infNameIntern === 'IN_INVENTORY') { // we need to add the checkbox for IN_INVENTORY defaulting to true $form->addInput( @@ -290,10 +291,10 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) public function createEditItemsForm(array $itemUUIDs = array()) { global $gCurrentSession, $gSettingsManager, $gCurrentUser, $gProfileFields, $gL10n, $gCurrentOrgId, $gDb; - // array with the internal field names of the lend fields not used in the edit form + // array with the internal field names of the borrow fields not used in the edit form // we also exclude IITEMNAME from the edit form, because it is only used for displaying item values based on the first entry // and it is not wanted to change the item name for multiple items at once - $lendFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + $borrowFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -329,8 +330,8 @@ public function createEditItemsForm(array $itemUUIDs = array()) foreach ($items->getItemFields() as $itemField) { $helpId = ''; $infNameIntern = $itemField->getValue('inf_name_intern'); - // Skip lend fields that are not used in the edit form - if (in_array($itemField->getValue('inf_name_intern'), $lendFieldNames)) { + // Skip borrow fields that are not used in the edit form + if (in_array($itemField->getValue('inf_name_intern'), $borrowFieldNames)) { if ($infNameIntern === 'ITEMNAME') { // If the item is new, we need to add the input for ITEMNAME $itemNames = ''; @@ -586,11 +587,11 @@ function toggleItemFields(fieldIdPrefix) { * @param string $itemFieldID ID of the item field that should be edited. * @throws Exception */ - public function createEditLendForm(string $itemUUID) + public function createEditBorrowForm(string $itemUUID) { global $gCurrentSession, $gSettingsManager, $gCurrentUser, $gL10n, $gCurrentOrgId, $gDb; - //array with the internal field names of the lend fields not used in the edit form - $lendFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrow fields not used in the edit form + $borrowFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -598,12 +599,12 @@ public function createEditLendForm(string $itemUUID) // Check if itemUUID is valid if (!Uuid::isValid($itemUUID)) { throw new Exception('The parameter "' . $itemUUID . '" is not a valid UUID!'); - } elseif ($gSettingsManager->GetBool('inventory_items_disable_lending')) { + } elseif ($gSettingsManager->GetBool('inventory_items_disable_borrowing')) { throw new Exception('SYS_INVALID_PAGE_VIEW'); } // display History button - ChangelogService::displayHistoryButton($this, 'inventory', 'inventory_item_lend_data', $gCurrentUser->isAdministratorInventory(), ['uuid' => $itemUUID]); + ChangelogService::displayHistoryButton($this, 'inventory', 'inventory_item_borrow_data', $gCurrentUser->isAdministratorInventory(), ['uuid' => $itemUUID]); // Read item data $items->readItemData($itemUUID); @@ -613,26 +614,10 @@ public function createEditLendForm(string $itemUUID) throw new Exception('SYS_NO_RIGHTS'); } - foreach ($items->getItemFields() as $itemField) { - $infNameIntern = $itemField->getValue('inf_name_intern'); - if($infNameIntern === 'IN_INVENTORY') { - $pimInInventory = $infNameIntern; - } - elseif($infNameIntern === 'LAST_RECEIVER') { - $pimLastReceiver = $infNameIntern; - } - elseif ($infNameIntern === 'RECEIVED_ON') { - $pimReceivedOn = $infNameIntern; - } - elseif ($infNameIntern === 'RECEIVED_BACK_ON') { - $pimReceivedBackOn = $infNameIntern; - } - } - // show form $form = new FormPresenter( - 'adm_item_edit_lend_form', - 'modules/inventory.item.edit.lend.tpl', + 'adm_item_edit_borrow_form', + 'modules/inventory.item.edit.borrow.tpl', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('item_uuid' => $itemUUID, 'mode' => 'item_save')), $this ); @@ -641,8 +626,21 @@ public function createEditLendForm(string $itemUUID) $helpId = ''; $infNameIntern = $itemField->getValue('inf_name_intern'); - // Skip all fields not used in the lend form - if (!in_array($infNameIntern, $lendFieldNames)) { + if($infNameIntern === 'IN_INVENTORY') { + $ivtInInventory = $infNameIntern; + } + elseif($infNameIntern === 'LAST_RECEIVER') { + $ivtLastReceiver = $infNameIntern; + } + elseif ($infNameIntern === 'BORROW_DATE') { + $ivtReceivedOn = $infNameIntern; + } + elseif ($infNameIntern === 'RETURN_DATE') { + $ivtReceivedBackOn = $infNameIntern; + } + + // Skip all fields not used in the borrow form + if (!in_array($infNameIntern, $borrowFieldNames)) { continue; } @@ -658,30 +656,30 @@ public function createEditLendForm(string $itemUUID) $fieldProperty = FormPresenter::FIELD_DISABLED; } - if (isset($pimInInventory, $pimLastReceiver, $pimReceivedOn, $pimReceivedBackOn) && $infNameIntern === 'IN_INVENTORY') { - // Add JavaScript to check the LAST_RECEIVER field and set the required attribute for pimReceivedOnId and pimReceivedBackOnId + if (isset($ivtInInventory, $ivtLastReceiver, $ivtReceivedOn, $ivtReceivedBackOn)) { + // Add JavaScript to check the LAST_RECEIVER field and set the required attribute for ivtReceivedOnId and ivtReceivedBackOnId $this->addJavascript(' document.addEventListener("DOMContentLoaded", function() { - if (document.querySelector("[id=\'INF-' . $pimReceivedOn . '_time\']")) { + if (document.querySelector("[id=\'INF-' . $ivtReceivedOn . '_time\']")) { var pDateTime = "true"; } else { var pDateTime = "false"; } - var pimInInventoryField = document.querySelector("[id=\'INF-' . $pimInInventory . '\']"); - var pimInInventoryGroup = document.getElementById("INF-' . $pimInInventory . '_group"); - var pimLastReceiverField = document.querySelector("[id=\'INF-' . $pimLastReceiver . '\']"); - var pimLastReceiverGroup = document.getElementById("INF-' . $pimLastReceiver . '_group"); - var pimReceivedOnField = document.querySelector("[id=\'INF-' . $pimReceivedOn . '\']"); + var ivtInInventoryField = document.querySelector("[id=\'INF-' . $ivtInInventory . '\']"); + var ivtInInventoryGroup = document.getElementById("INF-' . $ivtInInventory . '_group"); + var ivtLastReceiverField = document.querySelector("[id=\'INF-' . $ivtLastReceiver . '\']"); + var ivtLastReceiverGroup = document.getElementById("INF-' . $ivtLastReceiver . '_group"); + var ivtReceivedOnField = document.querySelector("[id=\'INF-' . $ivtReceivedOn . '\']"); if (pDateTime === "true") { - var pimReceivedOnFieldTime = document.querySelector("[id=\'INF-' . $pimReceivedOn . '_time\']"); - var pimReceivedBackOnFieldTime = document.querySelector("[id=\'INF-' . $pimReceivedBackOn . '_time\']"); + var ivtReceivedOnFieldTime = document.querySelector("[id=\'INF-' . $ivtReceivedOn . '_time\']"); + var ivtReceivedBackOnFieldTime = document.querySelector("[id=\'INF-' . $ivtReceivedBackOn . '_time\']"); } - var pimReceivedOnGroup = document.getElementById("INF-' . $pimReceivedOn . '_group"); - var pimReceivedBackOnField = document.querySelector("[id=\'INF-' . $pimReceivedBackOn . '\']"); - var pimReceivedBackOnGroup = document.getElementById("INF-' . $pimReceivedBackOn . '_group"); + var ivtReceivedOnGroup = document.getElementById("INF-' . $ivtReceivedOn . '_group"); + var ivtReceivedBackOnField = document.querySelector("[id=\'INF-' . $ivtReceivedBackOn . '\']"); + var ivtReceivedBackOnGroup = document.getElementById("INF-' . $ivtReceivedBackOn . '_group"); function setRequired(field, group, required) { if (required) { @@ -693,90 +691,90 @@ function setRequired(field, group, required) { } } - window.checkPimInInventory = function() { - var isInInventoryChecked = pimInInventoryField.checked; - var lastReceiverValue = pimLastReceiverField.value; - var receivedBackOnValue = pimReceivedBackOnField.value; + window.checkivtInInventory = function() { + var isInInventoryChecked = ivtInInventoryField.checked; + var lastReceiverValue = ivtLastReceiverField.value; + var receivedBackOnValue = ivtReceivedBackOnField.value; - setRequired(pimReceivedOnField, pimReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - setRequired(pimReceivedBackOnField, pimReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); + setRequired(ivtReceivedOnField, ivtReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); + setRequired(ivtReceivedBackOnField, ivtReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); if (pDateTime === "true") { - setRequired(pimReceivedOnFieldTime, pimReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - setRequired(pimReceivedBackOnFieldTime, pimReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); + setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); + setRequired(ivtReceivedBackOnFieldTime, ivtReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); } - setRequired(pimLastReceiverField, pimLastReceiverGroup, !isInInventoryChecked); - setRequired(pimReceivedOnField, pimReceivedOnGroup, !isInInventoryChecked); + setRequired(ivtLastReceiverField, ivtLastReceiverGroup, !isInInventoryChecked); + setRequired(ivtReceivedOnField, ivtReceivedOnGroup, !isInInventoryChecked); if (pDateTime === "true") { - setRequired(pimReceivedOnFieldTime, pimReceivedOnGroup, !isInInventoryChecked); + setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, !isInInventoryChecked); } if (!isInInventoryChecked && (lastReceiverValue === "undefined" || !lastReceiverValue)) { - pimReceivedOnField.value = ""; + ivtReceivedOnField.value = ""; if (pDateTime === "true") { - pimReceivedOnFieldTime.value = ""; + ivtReceivedOnFieldTime.value = ""; } } if (receivedBackOnValue !== "") { - setRequired(pimLastReceiverField, pimLastReceiverGroup, true); - setRequired(pimReceivedOnField, pimReceivedOnGroup, true); + setRequired(ivtLastReceiverField, ivtLastReceiverGroup, true); + setRequired(ivtReceivedOnField, ivtReceivedOnGroup, true); if (pDateTime === "true") { - setRequired(pimReceivedOnFieldTime, pimReceivedOnGroup, true); - setRequired(pimReceivedBackOnFieldTime, pimReceivedBackOnGroup, true); + setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, true); + setRequired(ivtReceivedBackOnFieldTime, ivtReceivedBackOnGroup, true); } } - var previousPimInInventoryState = isInInventoryChecked; + var previousivtInInventoryState = isInInventoryChecked; - pimInInventoryField.addEventListener("change", function() { - if (!pimInInventoryField.checked && previousPimInInventoryState) { - pimReceivedBackOnField.value = ""; + ivtInInventoryField.addEventListener("change", function() { + if (!ivtInInventoryField.checked && previousivtInInventoryState) { + ivtReceivedBackOnField.value = ""; if (pDateTime === "true") { - pimReceivedBackOnFieldTime.value = ""; + ivtReceivedBackOnFieldTime.value = ""; } } - previousPimInInventoryState = pimInInventoryField.checked; - window.checkPimInInventory(); + previousivtInInventoryState = ivtInInventoryField.checked; + window.checkivtInInventory(); }); - pimLastReceiverField.addEventListener("change", window.checkPimInInventory); - pimReceivedBackOnField.addEventListener("input", window.checkPimInInventory); - pimReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); + ivtLastReceiverField.addEventListener("change", window.checkivtInInventory); + ivtReceivedBackOnField.addEventListener("input", window.checkivtInInventory); + ivtReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); if (pDateTime === "true") { - pimReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); - pimReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); } } function validateReceivedOnAndBackOn() { if (pDateTime === "true") { - var receivedOnDate = new Date(pimReceivedOnField.value + " " + pimReceivedOnFieldTime.value); - var receivedBackOnDate = new Date(pimReceivedBackOnField.value + " " + pimReceivedBackOnFieldTime.value); + var receivedOnDate = new Date(ivtReceivedOnField.value + " " + ivtReceivedOnFieldTime.value); + var receivedBackOnDate = new Date(ivtReceivedBackOnField.value + " " + ivtReceivedBackOnFieldTime.value); } else { - var receivedOnDate = new Date(pimReceivedOnField.value); - var receivedBackOnDate = new Date(pimReceivedBackOnField.value); + var receivedOnDate = new Date(ivtReceivedOnField.value); + var receivedBackOnDate = new Date(ivtReceivedBackOnField.value); } if (receivedOnDate > receivedBackOnDate) { - pimReceivedOnField.setCustomValidity("ReceivedOn date cannot be after ReceivedBack date."); + ivtReceivedOnField.setCustomValidity("ReceivedOn date cannot be after ReceivedBack date."); } else { - pimReceivedOnField.setCustomValidity(""); + ivtReceivedOnField.setCustomValidity(""); } } - pimInInventoryField.addEventListener("change", window.checkPimInInventory); - pimLastReceiverField.addEventListener("change", window.checkPimInInventory); + ivtInInventoryField.addEventListener("change", window.checkivtInInventory); + ivtLastReceiverField.addEventListener("change", window.checkivtInInventory); - pimReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); - pimReceivedBackOnField.addEventListener("input", window.checkPimInInventory); + ivtReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); + ivtReceivedBackOnField.addEventListener("input", window.checkivtInInventory); if (pDateTime === "true") { - pimReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); - pimReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); } - pimReceivedBackOnField.addEventListener("input", validateReceivedOnAndBackOn); - window.checkPimInInventory(); + ivtReceivedBackOnField.addEventListener("input", validateReceivedOnAndBackOn); + window.checkivtInInventory(); }); '); } @@ -835,16 +833,16 @@ function validateReceivedOnAndBackOn() { ); $this->addJavascript(' - var selectIdLastReceiver = "#INF-' . $pimLastReceiver . '"; + var selectIdLastReceiver = "#INF-' . $ivtLastReceiver . '"; var defaultValue = "' . htmlspecialchars($items->getValue($infNameIntern)) . '"; var defaultText = "' . htmlspecialchars($items->getValue($infNameIntern)) . '"; // Der Text für den Default-Wert function isSelect2Empty(selectId) { // Hole den aktuellen Wert des Select2-Feldes - var renderedElement = $("#select2-INF-' . $pimLastReceiver .'-container"); + var renderedElement = $("#select2-INF-' . $ivtLastReceiver .'-container"); if (renderedElement.length) { - window.checkPimInInventory(); + window.checkivtInInventory(); } } // Prüfe, ob der Default-Wert in den Optionen enthalten ist @@ -854,7 +852,7 @@ function isSelect2Empty(selectId) { $(selectIdLastReceiver).append(newOption).trigger("change"); } - $("#INF-' . $pimLastReceiver .'").select2({ + $("#INF-' . $ivtLastReceiver .'").select2({ theme: "bootstrap-5", allowClear: true, placeholder: "", @@ -873,6 +871,13 @@ function isSelect2Empty(selectId) { if ($items->getProperty($infNameIntern, 'inf_type') === 'DATE') { $fieldType = $gSettingsManager->getString('inventory_field_date_time_format'); $maxlength = null; + $date = new DateTime('now'); + if ($fieldType === 'datetime') { + $defaultDate = $date->format('Y-m-d H:i'); + } else { + $defaultDate = $date->format('Y-m-d'); + } + } elseif ($infNameIntern === 'ITEMNAME') { $fieldProperty = FormPresenter::FIELD_DISABLED; @@ -884,7 +889,7 @@ function isSelect2Empty(selectId) { $form->addInput( 'INF-' . $infNameIntern, $items->getProperty($infNameIntern, 'inf_name'), - $items->getValue($infNameIntern), + ($items->getValue($infNameIntern) === '' && $infNameIntern === 'BORROW_DATE') ? $defaultDate : $items->getValue($infNameIntern), array( 'type' => $fieldType, 'maxLength' => isset($maxlength) ? $maxlength : null, diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index b61dcf45fb..0ea8c19872 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -101,7 +101,7 @@ protected function createHeader(): void if ($gCurrentUser->isAdministratorInventory()) { // show link to view inventory history - ChangelogService::displayHistoryButton($this, 'inventory', 'inventory_fields,inventory_field_select_options,inventory_items,inventory_item_data,inventory_item_lend_data'); + ChangelogService::displayHistoryButton($this, 'inventory', 'inventory_fields,inventory_field_select_options,inventory_items,inventory_item_data,inventory_item_borrow_data'); // show link to create new item $this->addPageFunctionsMenuItem( @@ -712,16 +712,16 @@ public function prepareData(string $mode = 'html') : array $headers = array(0 => ''); $exportHeaders = array(); $columnNumber = 1; - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrowing fields + $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); // Build headers and column alignment for each item field foreach ($this->itemsData->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); $columnHeader = $this->itemsData->getProperty($infNameIntern, 'inf_name'); - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } // For the first column, add specific header configurations for export modes @@ -785,8 +785,8 @@ public function prepareData(string $mode = 'html') : array foreach ($this->itemsData->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } // Apply filters for CATEGORY and KEEPER @@ -895,7 +895,7 @@ public function prepareData(string $mode = 'html') : array // Append admin action column for HTML mode if ($mode === 'html') { $historyButton = ChangelogService::displayHistoryButtonTable( - 'inventory_items,inventory_item_data,inventory_item_lend_data', + 'inventory_items,inventory_item_data,inventory_item_borrow_data', $gCurrentUser->isAdministratorInventory(), ['uuid' => $item['ini_uuid']] ); @@ -913,21 +913,21 @@ public function prepareData(string $mode = 'html') : array 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); - // Add lend action - if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { + // Add borrow action + if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { - $itemLended = false; + $item_borrowed = false; $icon ='bi bi-box-arrow-right'; - $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_LEND'); + $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); } else { - $itemLended = true; + $item_borrowed = true; $icon = 'bi bi-box-arrow-in-left'; $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_RETURN'); } $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit_lend', 'item_uuid' => $item['ini_uuid'], 'item_lended' => $itemLended)), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit_borrow', 'item_uuid' => $item['ini_uuid'], 'item_borrowed' => $item_borrowed)), 'icon' => $icon, 'tooltip' =>$tooltip ); @@ -1068,8 +1068,8 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $columnAlign[] = 'end'; // first column alignment $headers = array(); $columnNumber = 1; - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrow fields + $borrowFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // create array with all column heading values $profileItemFields = array('ITEMNAME'); @@ -1084,7 +1084,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter foreach ($itemsData->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); - if (!in_array($infNameIntern, $profileItemFields, true) || ($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames))) { + if (!in_array($infNameIntern, $profileItemFields, true) || ($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowFieldNames))) { continue; } @@ -1139,7 +1139,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter foreach ($itemsData->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); - if (!in_array($infNameIntern, $profileItemFields, true) || ($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames))) { + if (!in_array($infNameIntern, $profileItemFields, true) || ($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowFieldNames))) { continue; } @@ -1194,7 +1194,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter // Append admin action column $historyButton = ChangelogService::displayHistoryButtonTable( - 'inventory_items,inventory_item_data,inventory_item_lend_data', + 'inventory_items,inventory_item_data,inventory_item_borrow_data', $gCurrentUser->isAdministratorInventory(), ['uuid' => $item['ini_uuid']] ); @@ -1213,20 +1213,20 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter ); // Add lend action - if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_lending')) { + if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { - $itemLended = false; + $item_borrowed = false; $icon ='bi bi-box-arrow-right'; - $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_LEND'); + $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); } else { - $itemLended = true; + $item_borrowed = true; $icon = 'bi bi-box-arrow-in-left'; $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_RETURN'); } $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit_lend', 'item_uuid' => $item['ini_uuid'], 'item_lended' => $itemLended)), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit_borrow', 'item_uuid' => $item['ini_uuid'], 'item_borrowed' => $item_borrowed)), 'icon' => $icon, 'tooltip' =>$tooltip ); diff --git a/src/UI/Presenter/PreferencesPresenter.php b/src/UI/Presenter/PreferencesPresenter.php index c2a500edb8..5c2b39925f 100644 --- a/src/UI/Presenter/PreferencesPresenter.php +++ b/src/UI/Presenter/PreferencesPresenter.php @@ -427,7 +427,7 @@ public function createChangelogForm(): string array( 'title' => $gL10n->get('SYS_HEADER_CONTENT_MODULES'), 'id' => 'content_modules', - 'tables' => array('files', 'folders', 'photos', 'announcements', 'events', 'rooms', 'forum_topics', 'forum_posts', 'inventory_fields', 'inventory_field_select_options', 'inventory_items', 'inventory_item_data', 'inventory_item_lend_data', 'links', 'others') + 'tables' => array('files', 'folders', 'photos', 'announcements', 'events', 'rooms', 'forum_topics', 'forum_posts', 'inventory_fields', 'inventory_field_select_options', 'inventory_items', 'inventory_item_data', 'inventory_item_borrow_data', 'links', 'others') ), array( 'title' => $gL10n->get('SYS_HEADER_PREFERENCES'), @@ -782,8 +782,8 @@ public function createInventoryForm(): string { global $gL10n, $gSettingsManager, $gDb, $gCurrentOrgId, $gCurrentSession, $gCurrentUser; $formValues = $gSettingsManager->getAll(); - //array with the internal field names of the lend fields - $lendFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'RECEIVED_ON', 'RECEIVED_BACK_ON'); + //array with the internal field names of the borrowing fields + $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); $formInventory = new FormPresenter( 'adm_preferences_form_inventory', @@ -836,10 +836,10 @@ public function createInventoryForm(): string ); $formInventory->addCheckbox( - 'inventory_items_disable_lending', - $gL10n->get('SYS_INVENTORY_ITEMS_DISABLE_LENDING'), - (bool) $formValues['inventory_items_disable_lending'], - array('helpTextId' => 'SYS_INVENTORY_ITEMS_DISABLE_LENDING_DESC') + 'inventory_items_disable_borrowing', + $gL10n->get('SYS_INVENTORY_ITEMS_DISABLE_BORROWING'), + (bool) $formValues['inventory_items_disable_borrowing'], + array('helpTextId' => 'SYS_INVENTORY_ITEMS_DISABLE_BORROWING_DESC') ); $formInventory->addCheckbox( @@ -862,8 +862,8 @@ public function createInventoryForm(): string $selectBoxEntries = array(); foreach ($items->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); - if($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames)) { - continue; // skip lending fields if lending is disabled + if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowingFieldNames)) { + continue; // skip borrowing fields if borrowing is disabled } $selectBoxEntries[$infNameIntern] = $itemField->getValue('inf_name'); } @@ -921,7 +921,7 @@ public function createInventoryForm(): string $selectBoxEntries = array(); foreach ($items->getItemFields() as $itemField) { $infNameIntern = $itemField->getValue('inf_name_intern'); - if ($itemField->getValue('inf_name_intern') == 'ITEMNAME' || ($gSettingsManager->GetBool('inventory_items_disable_lending') && in_array($infNameIntern, $lendFieldNames))) { + if ($itemField->getValue('inf_name_intern') == 'ITEMNAME' || ($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($infNameIntern, $borrowingFieldNames))) { continue; } $selectBoxEntries[$infNameIntern] = $itemField->getValue('inf_name'); diff --git a/system/bootstrap/constants.php b/system/bootstrap/constants.php index 53faa0a264..93aaf57023 100755 --- a/system/bootstrap/constants.php +++ b/system/bootstrap/constants.php @@ -160,7 +160,7 @@ const TBL_INVENTORY_FIELDS = TABLE_PREFIX . '_inventory_fields'; const TBL_INVENTORY_FIELD_OPTIONS = TABLE_PREFIX . '_inventory_field_select_options'; const TBL_INVENTORY_ITEMS = TABLE_PREFIX . '_inventory_items'; -const TBL_INVENTORY_ITEM_LEND_DATA = TABLE_PREFIX . '_inventory_item_lend_data'; +const TBL_INVENTORY_ITEM_BORROW_DATA = TABLE_PREFIX . '_inventory_item_borrow_data'; // ##################### // ### OTHER STUFF ### diff --git a/themes/simple/templates/modules/inventory.item.edit.lend.tpl b/themes/simple/templates/modules/inventory.item.edit.borrow.tpl similarity index 100% rename from themes/simple/templates/modules/inventory.item.edit.lend.tpl rename to themes/simple/templates/modules/inventory.item.edit.borrow.tpl diff --git a/themes/simple/templates/preferences/preferences.inventory.tpl b/themes/simple/templates/preferences/preferences.inventory.tpl index 462aca6e64..d118e48c13 100644 --- a/themes/simple/templates/preferences/preferences.inventory.tpl +++ b/themes/simple/templates/preferences/preferences.inventory.tpl @@ -36,7 +36,7 @@ {include 'sys-template-parts/form.input.tpl' data=$elements['inventory_field_history_days']} {include 'sys-template-parts/form.seperator.tpl' data=$elements['inventory_seperator_general_settings']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_show_obsolete_select_field_options']} - {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_items_disable_lending']} + {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_items_disable_borrowing']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_system_field_names_editable']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_allow_keeper_edit']} {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_allowed_keeper_edit_fields']} From 9fdb3f04f5885d5686244d76e521734fa43edf26 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Thu, 31 Jul 2025 17:53:04 +0200 Subject: [PATCH 03/25] implement 'DROPDOWN_MULTISELECT' field --- src/Inventory/Entity/ItemData.php | 2 +- src/Inventory/Entity/ItemField.php | 2 +- src/Inventory/Service/ItemService.php | 4 +- src/Inventory/ValueObjects/ItemsData.php | 64 ++++++++++++++++++ src/UI/Presenter/InventoryFieldsPresenter.php | 4 +- src/UI/Presenter/InventoryItemPresenter.php | 65 ++++++++++++++----- src/UI/Presenter/InventoryPresenter.php | 4 +- 7 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/Inventory/Entity/ItemData.php b/src/Inventory/Entity/ItemData.php index 59aff3c6c5..8e1985728d 100644 --- a/src/Inventory/Entity/ItemData.php +++ b/src/Inventory/Entity/ItemData.php @@ -88,7 +88,7 @@ protected function logItemfieldChange(?string $oldval = null, ?string $newval = // Category changes are logged in the inventory items table return true; } - elseif ($infType === 'DROPDOWN' || $infType === 'RADIOBUTTON') { + elseif ($infType === 'DROPDOWN' || $infType === 'DROPDOWN_MULTISELECT' || $infType === 'RADIOBUTTON') { $vallist = $this->mItemsData->getProperty($fieldNameIntern, 'ifo_inf_options'); if (isset($vallist[$oldval])) { $oldval = $vallist[$oldval]; diff --git a/src/Inventory/Entity/ItemField.php b/src/Inventory/Entity/ItemField.php index b4f881e685..0ece3402f3 100644 --- a/src/Inventory/Entity/ItemField.php +++ b/src/Inventory/Entity/ItemField.php @@ -162,7 +162,7 @@ public function getValue($fieldNameIntern, $format = '', bool $withObsoleteEnrie break; case 'ifo_inf_options': - if ($this->dbColumns['inf_type'] === 'DROPDOWN' || $this->dbColumns['inf_type'] === 'RADIO_BUTTON') { + if ($this->dbColumns['inf_type'] === 'DROPDOWN' || $this->dbColumns['inf_type'] === 'DROPDOWN_MULTISELECT' || $this->dbColumns['inf_type'] === 'RADIO_BUTTON') { $arrOptionValuesWithKeys = array(); // array with option values and keys that represents the internal value $arrOptions = $value; diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index ec1526f7d0..e5e90ea9d7 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -134,7 +134,9 @@ public function save(bool $multiEdit = false): void $postKey = 'INF-' . $infNameIntern; if (isset($formValues[$postKey])) { - if (strlen($formValues[$postKey]) === 0 && $itemField->getValue('inf_required_input') == 1) { + if (is_array($formValues[$postKey]) && empty($formValues[$postKey])) { + throw new Exception($gL10n->get('SYS_FIELD_EMPTY', array($itemField->getValue('inf_name')))); + } elseif (is_string($formValues[$postKey]) && (strlen($formValues[$postKey]) === 0 && $itemField->getValue('inf_required_input') == 1)) { throw new Exception($gL10n->get('SYS_FIELD_EMPTY', array($itemField->getValue('inf_name')))); } diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index 091bc29c34..48db5c8e12 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -539,6 +539,47 @@ public function getHtmlValue($fieldNameIntern, $value): string } break; + case 'DROPDOWN_MULTISELECT': + $arrOptionValuesWithKeys = array(); // array with option values and keys that represents the internal value + $arrOptions = $this->mItemFields[$fieldNameIntern]->getValue('ifo_inf_options', 'database', false); + + foreach ($arrOptions as $values) { + // if text is a translation-id then translate it + $values['value'] = Language::translateIfTranslationStrId($values['value']); + + // save values in new array that starts with key = 1 + $arrOptionValuesWithKeys[$values['id']] = $values['value']; + } + + if (count($arrOptionValuesWithKeys) > 0 && !empty($value)) { + // split value by comma and trim each value + $valueArray = explode(',', $value); + foreach ($valueArray as &$val) { + $val = trim($val); + } + unset($val); + + // now create html output for each value + $htmlValue = ''; + foreach ($valueArray as $val) { + if (array_key_exists($val, $arrOptionValuesWithKeys)) { + // if value is the index of the array then we can use it + if ($htmlValue !== '') { + $htmlValue .= ', '; + } + $htmlValue .= $arrOptionValuesWithKeys[$val]; + } else { + if ($htmlValue !== '') { + $htmlValue .= ', '; + } + $htmlValue .= '' . $gL10n->get('SYS_DELETED_ENTRY') . ''; + } + } + } else { + $htmlValue = ''; + } + break; + case 'TEXT_BIG': $htmlValue = nl2br($value); break; @@ -673,6 +714,24 @@ public function getValue($fieldNameIntern, $format = '') $value = $arrOptions[$value]; } break; + + case 'DROPDOWN_MULTISELECT': + // the value in db is a comma separated list of positions, now search for the text + if ($value !== '' && $format !== 'html') { + $arrOptions = $this->mItemFields[$fieldNameIntern]->getValue('ifo_inf_options', $format, false); + $valueArray = explode(',', $value); + foreach ($valueArray as &$val) { + $val = trim($val); + if (array_key_exists($val, $arrOptions)) { + $val = $arrOptions[$val]; + } else { + $val = '' . $GLOBALS['gL10n']->get('SYS_DELETED_ENTRY') . ''; + } + } + unset($val); + $value = implode(', ', $valueArray); + } + break; } } } @@ -761,6 +820,11 @@ public function setValue($fieldNameIntern, $newValue): bool } } + if (is_array($newValue)) { + // if new value is an array then convert it to a string + $newValue = implode(',', $newValue); + } + // check if new value only contains spaces $newValue = (trim((string)$newValue) !== '') ? (string)$newValue : ''; diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index 241550ab68..c6224907d0 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -60,7 +60,7 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName $this->addJavascript(' $("#inf_type").change(function() { - if ($("#inf_type").val() === "DROPDOWN" || $("#inf_type").val() === "RADIO_BUTTON") { + if ($("#inf_type").val() === "DROPDOWN" || $("#inf_type").val() === "DROPDOWN_MULTISELECT" || $("#inf_type").val() === "RADIO_BUTTON") { $("#ifo_inf_options_table").attr("required", "required"); $("#ifo_inf_options_group").addClass("admidio-form-group-required"); $("#ifo_inf_options_group").show("slow"); @@ -122,6 +122,7 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName 'DATE' => $gL10n->get('SYS_DATE'), 'DECIMAL' => $gL10n->get('SYS_DECIMAL_NUMBER'), 'DROPDOWN' => $gL10n->get('SYS_DROPDOWN_LISTBOX'), + 'DROPDOWN_MULTISELECT' => $gL10n->get('SYS_DROPDOWN_MULTISELECT_LISTBOX'), 'EMAIL' => $gL10n->get('SYS_EMAIL'), 'NUMBER' => $gL10n->get('SYS_NUMBER'), 'PHONE' => $gL10n->get('SYS_PHONE'), @@ -270,6 +271,7 @@ public function createList() 'CHECKBOX' => $gL10n->get('SYS_CHECKBOX'), 'DATE' => $gL10n->get('SYS_DATE'), 'DROPDOWN' => $gL10n->get('SYS_DROPDOWN_LISTBOX'), + 'DROPDOWN_MULTISELECT' => $gL10n->get('SYS_DROPDOWN_MULTISELECT_LISTBOX'), 'EMAIL' => $gL10n->get('SYS_EMAIL'), 'RADIO_BUTTON' => $gL10n->get('SYS_RADIO_BUTTON'), 'PHONE' => $gL10n->get('SYS_PHONE'), diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index fb4cdbf6f4..368bc37f70 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -124,20 +124,31 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) ); break; - case 'DROPDOWN': - $form->addSelectBox( + case 'DROPDOWN': // fallthrough + case 'DROPDOWN_MULTISELECT': + $arrOptions = $items->getProperty($infNameIntern, 'ifo_inf_options', '', false); + $defaultValue = $items->getValue($infNameIntern, 'database'); + // prevent adding an empty string to the selectbox + if ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') { + // prevent adding an empty string to the selectbox + $defaultValue = ($defaultValue !== "") ? explode(',', $defaultValue) : array(); + } + + $form->addSelectBox( 'INF-' . $infNameIntern, $items->getProperty($infNameIntern, 'inf_name'), - $items->getProperty($infNameIntern, 'ifo_inf_options'), + $arrOptions, array( 'property' => $fieldProperty, - 'defaultValue' => $items->getValue($infNameIntern, 'database'), + 'defaultValue' => $defaultValue, 'helpTextId' => $helpId, - 'icon' => $items->getProperty($infNameIntern, 'inf_icon', 'database') + 'icon' => $items->getProperty($infNameIntern, 'inf_icon', 'database'), + 'multiselect' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? true : false, + 'maximumSelectionNumber' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? count($arrOptions) : 0, ) ); break; - + case 'RADIO_BUTTON': $form->addRadioButton( 'INF-' . $infNameIntern, @@ -386,17 +397,28 @@ public function createEditItemsForm(array $itemUUIDs = array()) ); break; - case 'DROPDOWN': - $form->addSelectBox( + case 'DROPDOWN': // fallthrough + case 'DROPDOWN_MULTISELECT': + $arrOptions = $items->getProperty($infNameIntern, 'ifo_inf_options', '', false); + $defaultValue = $items->getValue($infNameIntern, 'database'); + // prevent adding an empty string to the selectbox + if ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') { + // prevent adding an empty string to the selectbox + $defaultValue = ($defaultValue !== "") ? explode(',', $defaultValue) : array(); + } + + $form->addSelectBox( 'INF-' . $infNameIntern, $items->getProperty($infNameIntern, 'inf_name'), - $items->getProperty($infNameIntern, 'ifo_inf_options'), + $arrOptions, array( 'property' => $fieldProperty, - 'defaultValue' => $items->getValue($infNameIntern, 'database'), + 'defaultValue' => $defaultValue, 'helpTextId' => $helpId, 'icon' => $items->getProperty($infNameIntern, 'inf_icon', 'database'), - 'toggleable' => true + 'toggleable' => true, + 'multiselect' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? true : false, + 'maximumSelectionNumber' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? count($arrOptions) : 0, ) ); break; @@ -797,16 +819,27 @@ function validateReceivedOnAndBackOn() { ); break; - case 'DROPDOWN': - $form->addSelectBox( + case 'DROPDOWN': // fallthrough + case 'DROPDOWN_MULTISELECT': + $arrOptions = $items->getProperty($infNameIntern, 'ifo_inf_options', '', false); + $defaultValue = $items->getValue($infNameIntern, 'database'); + // prevent adding an empty string to the selectbox + if ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') { + // prevent adding an empty string to the selectbox + $defaultValue = ($defaultValue !== "") ? explode(',', $defaultValue) : array(); + } + + $form->addSelectBox( 'INF-' . $infNameIntern, $items->getProperty($infNameIntern, 'inf_name'), - $items->getProperty($infNameIntern, 'ifo_inf_options'), + $arrOptions, array( 'property' => $fieldProperty, - 'defaultValue' => $items->getValue($infNameIntern, 'database'), + 'defaultValue' => $defaultValue, 'helpTextId' => $helpId, - 'icon' => $items->getProperty($infNameIntern, 'inf_icon', 'database') + 'icon' => $items->getProperty($infNameIntern, 'inf_icon', 'database'), + 'multiselect' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? true : false, + 'maximumSelectionNumber' => ($items->getProperty($infNameIntern, 'inf_type') === 'DROPDOWN_MULTISELECT') ? count($arrOptions) : 0, ) ); break; diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 0ea8c19872..12cf14fa5e 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -874,7 +874,7 @@ public function prepareData(string $mode = 'html') : array $content = in_array($mode, ['csv', 'pdf', 'xlsx', 'ods']) ? ($content == 1 ? $gL10n->get('SYS_YES') : $gL10n->get('SYS_NO')) : $this->itemsData->getHtmlValue($infNameIntern, $content); - } elseif (in_array($infType, ['DATE', 'DROPDOWN'])) { + } elseif (in_array($infType, ['DATE', 'DROPDOWN', 'DROPDOWN_MULTISELECT'])) { $content = $this->itemsData->getHtmlValue($infNameIntern, $content); } elseif ($infType === 'RADIO_BUTTON') { $content = $mode === 'html' @@ -1180,7 +1180,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter if ($infType === 'CHECKBOX') { $content = ($content != 1) ? 0 : 1; $content = $itemsData->getHtmlValue($infNameIntern, $content); - } elseif (in_array($infType, ['DATE', 'DROPDOWN'])) { + } elseif (in_array($infType, ['DATE', 'DROPDOWN', 'DROPDOWN_MULTISELECT'])) { $content = $itemsData->getHtmlValue($infNameIntern, $content); } elseif ($infType === 'RADIO_BUTTON') { $content = $itemsData->getHtmlValue($infNameIntern, $content); From ede2492a99c12cf6526fec8d98abe5399869f3fa Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Thu, 31 Jul 2025 21:26:40 +0200 Subject: [PATCH 04/25] feat: add possibility for system fields to SelectOptions --- install/db_scripts/update_5_0.xml | 1 + src/Inventory/Entity/SelectOptions.php | 10 +++++++-- src/ProfileFields/Entity/SelectOptions.php | 10 +++++++-- src/UI/Presenter/FormPresenter.php | 22 +++++++++++++++---- src/UI/Presenter/InventoryFieldsPresenter.php | 2 +- src/UI/Presenter/ProfileFieldsPresenter.php | 2 +- system/js/common_functions.js | 15 ++++++++----- .../sys-template-parts/form.option-editor.tpl | 20 ++++++++++++++++- 8 files changed, 66 insertions(+), 16 deletions(-) diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index cb0e11b5de..65f4ebf8e0 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -492,6 +492,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ifo_id integer unsigned NOT NULL AUTO_INCREMENT, ifo_inf_id integer unsigned NOT NULL, ifo_value varchar(255) NOT NULL, + ifo_system boolean NOT NULL DEFAULT false, ifo_sequence smallint NOT NULL, ifo_obsolete boolean NOT NULL DEFAULT false, PRIMARY KEY (ifo_id) diff --git a/src/Inventory/Entity/SelectOptions.php b/src/Inventory/Entity/SelectOptions.php index 2eee461c64..71c525d630 100644 --- a/src/Inventory/Entity/SelectOptions.php +++ b/src/Inventory/Entity/SelectOptions.php @@ -55,7 +55,7 @@ public function readDataByFieldId(int $infId = 0) : void // if id is set than read the data of the recordset if ($this->infId > 0) { - $sql = 'SELECT ifo_id, ifo_value, ifo_sequence, ifo_obsolete + $sql = 'SELECT ifo_id, ifo_value, ifo_system, ifo_sequence, ifo_obsolete FROM ' . TBL_INVENTORY_FIELD_OPTIONS . ' WHERE ifo_inf_id = ? -- $infId ORDER BY ifo_sequence'; @@ -104,6 +104,7 @@ public function getAllOptions(bool $withObsoleteEnries = true) : array $values[$value['ifo_id']] = array( 'id' => $value['ifo_id'], 'value' => $value['ifo_value'], + 'system' => $value['ifo_system'], 'sequence' => $value['ifo_sequence'], 'obsolete' => $value['ifo_obsolete'] ); @@ -252,10 +253,15 @@ public function setOptionValues(array $newValues) : bool $currentSequence = array(); foreach ($allOptions as $option) { $currentSequence[$option['id']] = $option['sequence'] - 1; // -1 because sequence starts with 1 in database + if ($option['system'] == 1) { + $lastSystemSequence = $option['sequence']; + } } // determinate new sequence based on array position $newSequence = array(); - $sequence = 0; + + // check if there are system options, if so then the sequence must start with the sequence of the last system option + $sequence = $lastSystemSequence ?? 0; foreach ($arrValues as $id => $values) { $newSequence[$id] = $sequence++; } diff --git a/src/ProfileFields/Entity/SelectOptions.php b/src/ProfileFields/Entity/SelectOptions.php index 74996d0f74..00380540c2 100644 --- a/src/ProfileFields/Entity/SelectOptions.php +++ b/src/ProfileFields/Entity/SelectOptions.php @@ -55,7 +55,7 @@ public function readDataByFieldId(int $usfId = 0) : void // if id is set than read the data of the recordset if ($this->usfId > 0) { - $sql = 'SELECT ufo_id, ufo_value, ufo_sequence, ufo_obsolete + $sql = 'SELECT ufo_id, ufo_value, ifo_system, ufo_sequence, ufo_obsolete FROM ' . TBL_USER_FIELD_OPTIONS . ' WHERE ufo_usf_id = ? -- $usfId ORDER BY ufo_sequence'; @@ -104,6 +104,7 @@ public function getAllOptions(bool $withObsoleteEnries = true) : array $values[$value['ufo_id']] = array( 'id' => $value['ufo_id'], 'value' => $value['ufo_value'], + 'system' => $value['ifo_system'], 'sequence' => $value['ufo_sequence'], 'obsolete' => $value['ufo_obsolete'] ); @@ -252,10 +253,15 @@ public function setOptionValues(array $newValues) : bool $currentSequence = array(); foreach ($allOptions as $option) { $currentSequence[$option['id']] = $option['sequence'] - 1; // -1 because sequence starts with 1 in database + if ($option['system'] == 1) { + $lastSystemSequence = $option['sequence']; + } } // determinate new sequence based on array position $newSequence = array(); - $sequence = 0; + + // check if there are system options, if so then the sequence must start with the sequence of the last system option + $sequence = $lastSystemSequence ?? 0; foreach ($arrValues as $id => $values) { $newSequence[$id] = $sequence++; } diff --git a/src/UI/Presenter/FormPresenter.php b/src/UI/Presenter/FormPresenter.php index 90b431155b..ab7599fcb2 100644 --- a/src/UI/Presenter/FormPresenter.php +++ b/src/UI/Presenter/FormPresenter.php @@ -998,11 +998,25 @@ public function addOptionEditor(string $id, string $label, array $values, array { global $gL10n; + // sort values based on sortable or not sortable (system) fields + $sortable = array(); + $notSortable = array(); + foreach ($values as $key => $value) { + if (!$value['system']) { + $sortable[$key] = $value; + } else { + $notSortable[$key] = $value; + } + } + $optionsAll = $this->buildOptionsArray(array_replace(array( 'type' => 'option-editor', 'id' => $id, 'label' => $label, - 'values' => $values, + 'values' => array( + 'sortable' => $sortable, + 'notSortable' => $notSortable + ), 'filename' => 'profile-fields' ), $options)); $attributes = array(); @@ -1037,10 +1051,10 @@ public function addOptionEditor(string $id, string $label, array $values, array $this->addJavascriptCode(' function addOptionRow(dataId, checkUrl, deleteUrl, csrfToken, translationStrings) { - const table = document.getElementById(dataId + "_table").getElementsByTagName("tbody")[0]; + const table = document.querySelector("#" + dataId + "_table tbody.admidio-sortable"); const newRow = document.createElement("tr"); const rows = table.querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\'); - let maxId = 0; + let maxId = document.querySelector("#" + dataId + "_table tbody.admidio-not-sortable").querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length; rows.forEach(row => { const currentId = row.id.replace(dataId + "_option_", ""); const num = parseInt(currentId, 10); @@ -1100,7 +1114,7 @@ function deleteEntry(dataId, entryId, checkUrl, deleteUrl, deleteMsg, csrfToken) if (!row) return; const table = row.parentNode; - const countOptions = table.querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length; + const countOptions = table.querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length + document.querySelector("#" + dataId + "_table tbody.admidio-not-sortable").querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length; // If there is only one option left, do not delete it or mark it as obsolete if (countOptions <= 1) return; diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index c6224907d0..834e23c62b 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -155,7 +155,7 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName $optionValueList = $options->getAllOptions($gSettingsManager->getBool('inventory_show_obsolete_select_field_options')); if (empty($optionValueList)) { $optionValueList = array( - 0 => array('id' => 1, 'value' => '', 'sequence' => 0, 'obsolete' => false) + 0 => array('id' => 1, 'value' => '', 'system' => false, 'sequence' => 0, 'obsolete' => false) ); } $form->addOptionEditor( diff --git a/src/UI/Presenter/ProfileFieldsPresenter.php b/src/UI/Presenter/ProfileFieldsPresenter.php index d998d3cb62..86e3215ec1 100644 --- a/src/UI/Presenter/ProfileFieldsPresenter.php +++ b/src/UI/Presenter/ProfileFieldsPresenter.php @@ -176,7 +176,7 @@ public function createEditForm(string $profileFieldUUID = '') $optionValueList = $options->getAllOptions($gSettingsManager->getBool('profile_show_obsolete_select_field_options')); if (empty($optionValueList)) { $optionValueList = array( - 0 => array('id' => 1, 'value' => '', 'sequence' => 0, 'obsolete' => false) + 0 => array('id' => 1, 'value' => '', 'system' => false, 'sequence' => 0, 'obsolete' => false) ); } $form->addOptionEditor( diff --git a/system/js/common_functions.js b/system/js/common_functions.js index a03d913ad3..7a0cdfe9ad 100644 --- a/system/js/common_functions.js +++ b/system/js/common_functions.js @@ -151,7 +151,9 @@ function callUrlHideElement(elementId, url, csrfToken, callback) { if (isTbodyEmpty(tbodyElement)) { $(tbodyElement).fadeOut("slow"); var tbodyElement2 = tbodyElement.previousElementSibling; - $(tbodyElement2).fadeOut("slow"); + if (isTbodyEmpty(tbodyElement2)) { + $(tbodyElement2).fadeOut("slow"); + } } } else { // entry could not be deleted, then show content of data or a common error message @@ -193,10 +195,13 @@ function callUrlHideElements(elementPrefix, elementIds, url, csrfToken) { $(entry).fadeOut("slow"); // then check if its is now empty - var tb = entry.closest("tbody"); - if (tb && tb.children.length === 0) { - $(tb).fadeOut("slow"); - $(tb.previousElementSibling).fadeOut("slow"); + var tbodyElement = entry.closest("tbody"); + if (isTbodyEmpty(tbodyElement)) { + $(tbodyElement).fadeOut("slow"); + var tbodyElement2 = tbodyElement.previousElementSibling; + if (isTbodyEmpty(tbodyElement2)) { + $(tbodyElement2).fadeOut("slow"); + } } } diff --git a/themes/simple/templates/sys-template-parts/form.option-editor.tpl b/themes/simple/templates/sys-template-parts/form.option-editor.tpl index 817d929f9b..36d1be96ec 100644 --- a/themes/simple/templates/sys-template-parts/form.option-editor.tpl +++ b/themes/simple/templates/sys-template-parts/form.option-editor.tpl @@ -36,8 +36,26 @@   + {if isset($data.values.notSortable) && count($data.values.notSortable) > 0} + + {foreach $data.values.notSortable as $option} + + + + + +
+ +
+ + + + + {/foreach} + + {/if} - {foreach $data.values as $option} + {foreach $data.values.sortable as $option} From 43c0a9baea3e9abe9d05d4b03e0e8df8389126c6 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Thu, 31 Jul 2025 21:52:01 +0200 Subject: [PATCH 05/25] feat: replace retiredproperty with a status field to exend possible item states. --- install/db_scripts/db.sql | 3 +- install/db_scripts/update_5_0.xml | 9 +- languages/en.xml | 2 + src/Changelog/Service/ChangelogService.php | 2 +- .../Service/UpdateStepsCode.php | 21 ++- src/Inventory/Entity/Item.php | 41 ++++++ src/Inventory/Entity/ItemBorrowData.php | 16 +++ src/Inventory/Service/ImportService.php | 14 +- src/Inventory/ValueObjects/ItemsData.php | 128 ++++++++++++++++-- src/UI/Presenter/InventoryFieldsPresenter.php | 6 +- src/UI/Presenter/InventoryPresenter.php | 78 ++++++----- .../templates/modules/inventory.list.tpl | 30 ++-- 12 files changed, 284 insertions(+), 66 deletions(-) diff --git a/install/db_scripts/db.sql b/install/db_scripts/db.sql index ff34b45c1d..1cb1774c0c 100644 --- a/install/db_scripts/db.sql +++ b/install/db_scripts/db.sql @@ -1086,7 +1086,7 @@ CREATE TABLE %PREFIX%_inventory_items ini_uuid varchar(36) NOT NULL, ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, - ini_retired boolean NOT NULL DEFAULT false, + ini_status integer unsigned NOT NULL, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, @@ -1361,5 +1361,6 @@ ALTER TABLE %PREFIX%_inventory_item_borrow_data ADD CONSTRAINT %PREFIX%_fk_inb_ini FOREIGN KEY (inb_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; ALTER TABLE %PREFIX%_inventory_items ADD CONSTRAINT %PREFIX%_fk_ini_cat FOREIGN KEY (ini_cat_id) REFERENCES %PREFIX%_categories (cat_id) ON DELETE RESTRICT ON UPDATE RESTRICT, + ADD CONSTRAINT %PREFIX%_fk_ini_status FOREIGN KEY (ini_status) REFERENCES %PREFIX%_inventory_field_select_options (ifo_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ini_usr_create FOREIGN KEY (ini_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ini_usr_change FOREIGN KEY (ini_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT; diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index 65f4ebf8e0..7771ad4e25 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -463,7 +463,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ini_uuid varchar(36) NOT NULL, ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, - ini_retired boolean NOT NULL DEFAULT false, + ini_status integer unsigned NOT NULL, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, @@ -475,9 +475,10 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N COLLATE = utf8_unicode_ci;
CREATE UNIQUE INDEX %PREFIX%_idx_ini_uuid ON %PREFIX%_inventory_items (ini_uuid); ALTER TABLE %PREFIX%_inventory_items - ADD CONSTRAINT %PREFIX%_fk_ini_cat FOREIGN KEY (ini_cat_id) REFERENCES %PREFIX%_categories (cat_id) ON DELETE RESTRICT ON UPDATE RESTRICT, - ADD CONSTRAINT %PREFIX%_fk_ini_usr_create FOREIGN KEY (ini_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, - ADD CONSTRAINT %PREFIX%_fk_ini_usr_change FOREIGN KEY (ini_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT; + ADD CONSTRAINT %PREFIX%_fk_ini_cat FOREIGN KEY (ini_cat_id) REFERENCES %PREFIX%_categories (cat_id) ON DELETE RESTRICT ON UPDATE RESTRICT, + ADD CONSTRAINT %PREFIX%_fk_ini_status FOREIGN KEY (ini_status) REFERENCES %PREFIX%_inventory_field_select_options (ifo_id) ON DELETE RESTRICT ON UPDATE RESTRICT, + ADD CONSTRAINT %PREFIX%_fk_ini_usr_create FOREIGN KEY (ini_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, + ADD CONSTRAINT %PREFIX%_fk_ini_usr_change FOREIGN KEY (ini_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT;
ALTER TABLE %PREFIX%_roles ADD COLUMN rol_inventory_admin boolean NOT NULL DEFAULT false UPDATE %PREFIX%_roles SET rol_inventory_admin = true WHERE rol_administrator = true INSERT INTO %PREFIX%_components (com_type, com_name, com_name_intern, com_version, com_beta) diff --git a/languages/en.xml b/languages/en.xml index 0b6d14ac8e..5811e7eaed 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -882,6 +882,8 @@ Retire selected items You can retire the item from the inventory manager by choosing #VAR1_BOLD#. This has the advantage that the data is preserved and you can always later see who borrowed the item. The selected items have been retired. + Status + The current status of the item Permission to edit the display name of system fields If this option is enabled, the display names of the system item fields can be modified. (Default: no) Current user as default selection diff --git a/src/Changelog/Service/ChangelogService.php b/src/Changelog/Service/ChangelogService.php index bc0ab2a15d..8a4d00b15d 100644 --- a/src/Changelog/Service/ChangelogService.php +++ b/src/Changelog/Service/ChangelogService.php @@ -523,7 +523,7 @@ public static function getFieldTranslations(): array 'inf_required_input' => array('name' => 'SYS_REQUIRED_INPUT', 'type' => 'BOOL'), 'inf_sequence' => 'SYS_ORDER', 'ini_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), - 'ini_retired' => array('name' => 'SYS_INVENTORY_ITEM_RETIRED_CHANGELOG', 'type' => 'BOOL'), + 'ini_status' => array('name' => 'SYS_INVENTORY_STATUS'), 'ind_value_bool' => array('name' => 'SYS_VALUE', 'type' => 'BOOL'), 'ind_value_date' => array('name' => 'SYS_VALUE', 'type' => 'DATE'), 'ind_value_mail' => array('name' => 'SYS_VALUE', 'type' => 'EMAIL'), diff --git a/src/InstallationUpdate/Service/UpdateStepsCode.php b/src/InstallationUpdate/Service/UpdateStepsCode.php index 4b0e1a508e..24b4604eb0 100644 --- a/src/InstallationUpdate/Service/UpdateStepsCode.php +++ b/src/InstallationUpdate/Service/UpdateStepsCode.php @@ -92,7 +92,8 @@ public static function updateStep50AddInventoryFields() array('inf_type' => 'CHECKBOX', 'inf_name_intern' => 'IN_INVENTORY', 'inf_name' => 'SYS_INVENTORY_IN_INVENTORY', 'inf_description' => 'SYS_INVENTORY_IN_INVENTORY_DESC', 'inf_required_input' => 0, 'inf_sequence' => 3), array('inf_type' => 'TEXT', 'inf_name_intern' => 'LAST_RECEIVER', 'inf_name' => 'SYS_INVENTORY_LAST_RECEIVER', 'inf_description' => 'SYS_INVENTORY_LAST_RECEIVER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 4), array('inf_type' => 'DATE', 'inf_name_intern' => 'BORROW_DATE', 'inf_name' => 'SYS_INVENTORY_BORROW_DATE', 'inf_description' => 'SYS_INVENTORY_BORROW_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 5), - array('inf_type' => 'DATE', 'inf_name_intern' => 'RETURN_DATE', 'inf_name' => 'SYS_INVENTORY_RETURN_DATE', 'inf_description' => 'SYS_INVENTORY_RETURN_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6) + array('inf_type' => 'DATE', 'inf_name_intern' => 'RETURN_DATE', 'inf_name' => 'SYS_INVENTORY_RETURN_DATE', 'inf_description' => 'SYS_INVENTORY_RETURN_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6), + array('inf_type' => 'DROPDOWN', 'inf_name_intern' => 'STATUS', 'inf_name' => 'SYS_INVENTORY_STATUS', 'inf_description' => 'SYS_INVENTORY_STATUS_DESC', 'inf_required_input' => 1, 'inf_sequence' => 7) ); $sql = 'SELECT org_id, org_shortname FROM ' . TBL_ORGANIZATIONS; @@ -114,6 +115,24 @@ public static function updateStep50AddInventoryFields() } } + // add default options for the status field + $sql = 'SELECT inf_id FROM ' . TBL_INVENTORY_FIELDS . ' + WHERE inf_name_intern = \'STATUS\''; + $statusFieldId = self::$db->queryPrepared($sql)->fetchColumn(); + + if ($statusFieldId !== false) { + $arrStatusOptions = array( + array('inf_name' => 'SYS_INVENTORY_FILTER_IN_USE_ITEMS', 'ifo_sequence' => 1), + array('inf_name' => 'SYS_INVENTORY_FILTER_RETIRED_ITEMS', 'ifo_sequence' => 2), + ); + + foreach ($arrStatusOptions as $statusOption) { + $sql = 'INSERT INTO ' . TBL_INVENTORY_FIELD_OPTIONS . ' + (ifo_inf_id, ifo_value, ifo_system, ifo_sequence) + VALUES (?, ?, ?, ?)'; + self::$db->queryPrepared($sql, array($statusFieldId, $statusOption['inf_name'], true, $statusOption['ifo_sequence'])); + } + } } /** diff --git a/src/Inventory/Entity/Item.php b/src/Inventory/Entity/Item.php index b7979531b2..8be042cdd0 100644 --- a/src/Inventory/Entity/Item.php +++ b/src/Inventory/Entity/Item.php @@ -6,6 +6,8 @@ use Admidio\Infrastructure\Database; use Admidio\Infrastructure\Entity\Entity; use Admidio\Inventory\ValueObjects\ItemsData; +use Admidio\Inventory\Entity\ItemData; +use Admidio\Inventory\Entity\SelectOptions; use Admidio\Changelog\Entity\LogChanges; /** @@ -129,6 +131,45 @@ public function readableName(): string $itemData->readDataByColumns(array('ind_ini_id' => $this->itemId, 'ind_inf_id' => $this->mItemsData->getProperty('ITEMNAME', 'inf_id'))); return $itemData->getValue('ind_value'); } + + /** + * Get the status of the item. + * @return int The status of the item. + */ + public function getStatus(): int + { + return $this->getValue('ini_status'); + } + + /** + * Check if the item is retired. + * @return bool Returns true if the item is retired, false otherwise. + */ + public function isRetired(): bool + { + global $gDb; + $optionId = $this->getStatus(); + $option = new SelectOptions($gDb); + if ($option->readDataById($optionId)) { + return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_RETIRED'; + } + return false; + } + + /** + * Check if the item is in use. + * @return bool Returns true if the item is in use, false otherwise. + */ + public function isInUse(): bool + { + global $gDb; + $optionId = $this->getStatus(); + $option = new SelectOptions($gDb); + if ($option->readDataById($optionId)) { + return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_IN_USE'; + } + return false; + } /** * Retrieve the list of database fields that are ignored for the changelog. diff --git a/src/Inventory/Entity/ItemBorrowData.php b/src/Inventory/Entity/ItemBorrowData.php index 7a034cc76f..783beba2b2 100644 --- a/src/Inventory/Entity/ItemBorrowData.php +++ b/src/Inventory/Entity/ItemBorrowData.php @@ -59,6 +59,22 @@ public function updateRecordId(int $recordId) : void } } + /** + * Retrieve the list of database fields that are ignored for the changelog. + * Some tables contain columns _usr_id_create, timestamp_create, etc. We do not want + * to log changes to these columns. + * The guestbook table also contains gbc_org_id and gbc_ip_address columns, + * which we don't want to log. + * @return array Returns the list of database columns to be ignored for logging. + */ + public function getIgnoredLogColumns(): array + { + return array_merge(parent::getIgnoredLogColumns(), + ['inb_id', 'inb_ini_id']/* , + ($this->newRecord)?[$this->columnPrefix.'_text']:[] */ + ); + } + /** * Adjust the changelog entry for this db record: Add the first forum post as a related object * @param LogChanges $logEntry The log entry to adjust diff --git a/src/Inventory/Service/ImportService.php b/src/Inventory/Service/ImportService.php index 32033e8848..522aa48618 100644 --- a/src/Inventory/Service/ImportService.php +++ b/src/Inventory/Service/ImportService.php @@ -14,8 +14,10 @@ use Admidio\Categories\Service\CategoryService; use Admidio\Categories\Entity\Category; use Admidio\Infrastructure\Exception; +use Admidio\Infrastructure\Language; use Admidio\Inventory\Service\ItemService; use Admidio\Inventory\ValueObjects\ItemsData; +use Admidio\Inventory\Entity\SelectOptions; use Admidio\Inventory\Entity\Item; // PHP namespaces @@ -341,7 +343,6 @@ public function importItems(): array $val = $category->getValue('cat_uuid'); } } - } elseif($imfNameIntern === 'BORROWING_DATE' || $imfNameIntern === 'RETURN_DATE') { $val = $values[$infId]; @@ -383,6 +384,17 @@ public function importItems(): array } } } + elseif($imfNameIntern === 'STATUS') { + $option = new SelectOptions($gDb, $infId); + $optionValues = $option->getAllOptions(); + $val = ''; + foreach ($optionValues as $optionData) { + if (Language::translateIfTranslationStrId($optionData['value']) === $values[$infId]) { + $val = $optionData['id']; + break; + } + } + } else { $val = $values[$infId]; } diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index 48db5c8e12..fc8b882dcb 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -15,6 +15,7 @@ use Admidio\Inventory\Entity\ItemField; use Admidio\Inventory\Entity\ItemBorrowData; use Admidio\Categories\Entity\Category; +use Admidio\Inventory\Entity\SelectOptions; // PHP namespaces use DateTime; @@ -237,10 +238,20 @@ public function readItems(): void $sqlWhereCondition = ''; if (!$this->showRetiredItems) { - $sqlWhereCondition .= 'AND ini_retired = 0'; + // get the option id of the retired status + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $retiredId = 0; + foreach ($values as $value) { + if ($value['value'] === 'SYS_INVENTORY_FILTER_RETIRED_ITEMS') { + $retiredId = $value['id']; + break; + } + } + $sqlWhereCondition .= 'AND ini_status NOT IN (' . $retiredId . ')'; } - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEMS . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_status FROM ' . TBL_INVENTORY_ITEMS . ' INNER JOIN ' . TBL_INVENTORY_ITEM_DATA . ' ON ind_ini_id = ini_id WHERE ini_org_id IS NULL @@ -249,7 +260,7 @@ public function readItems(): void $statement = $this->mDb->queryPrepared($sql, array($this->organizationId)); while ($row = $statement->fetch()) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } } @@ -268,7 +279,17 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void $sqlWhereCondition = ''; if (!$this->showRetiredItems) { - $sqlWhereCondition .= 'AND ini_retired = 0'; + // get the option id of the retired status + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $retiredId = 0; + foreach ($values as $value) { + if ($value['value'] === 'SYS_INVENTORY_FILTER_RETIRED_ITEMS') { + $retiredId = $value['id']; + break; + } + } + $sqlWhereCondition .= 'AND ini_status = ' . $retiredId; } $sqlImfIds = 'AND ('; @@ -280,7 +301,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void } // first read all item data for the given user - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_DATA . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_status FROM ' . TBL_INVENTORY_ITEM_DATA . ' INNER JOIN ' . TBL_INVENTORY_FIELDS . ' ON inf_id = ind_inf_id ' . $sqlImfIds . ' @@ -293,11 +314,11 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); while ($row = $statement->fetch()) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } // now read the item borrow data for each item - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_retired FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_status FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' INNER JOIN ' . TBL_INVENTORY_ITEMS . ' ON ini_id = inb_ini_id WHERE (ini_org_id IS NULL @@ -317,7 +338,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void } // if item doesn't exist, then add it to the items array if (!$itemExists) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_retired' => $row['ini_retired']); + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } } } @@ -659,6 +680,23 @@ public function getValue($fieldNameIntern, $format = '') } } } + elseif ($fieldNameIntern === 'STATUS') { + // special case for status + $item = new Item($this->mDb, $this, $this->mItemId); + $statusId = $item->getValue('ini_status'); + if ($statusId > 0) { + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + foreach ($values as $valueArray) { + if ($valueArray['id'] === $statusId) { + if ($format === 'database') { + return $valueArray['id']; + } + return Language::translateIfTranslationStrId($valueArray['value']); + } + } + } + } elseif (array_key_exists($this->mItemFields[$fieldNameIntern]->getValue('inf_id'), $this->mItemData)) { if ($this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')] instanceof ItemBorrowData) { $value = $this->mItemData[$this->mItemFields[$fieldNameIntern]->getValue('inf_id')]->getValue('inb_' . strtolower($fieldNameIntern), $format); @@ -744,6 +782,14 @@ public function getValue($fieldNameIntern, $format = '') return $value; } + public function getStatus(): int + { + $item = new Item($this->mDb, $this); + $item->readDataByUuid($this->mItemUUID); + + return $item->getStatus(); + } + /** * Marks an item as imported. * @@ -769,6 +815,28 @@ public function showRetiredItems($newValue = null): bool return $this->showRetiredItems; } + public function isRetired(): bool + { + global $gDb; + $optionId = $this->getStatus(); + $option = new SelectOptions($gDb); + if ($option->readDataById($optionId)) { + return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_RETIRED'; + } + return false; + } + + public function isInUse(): bool + { + global $gDb; + $optionId = $this->getStatus(); + $option = new SelectOptions($gDb); + if ($option->readDataById($optionId)) { + return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_IN_USE'; + } + return false; + } + /** * If the recordset is new and wasn't read from database or was not stored in database * then this method will return true otherwise false @@ -911,9 +979,20 @@ public function createNewItem(string $catUUID): void $category = new Category($this->mDb); $category->readDataByUuid($catUUID); + // get the option id of the in use status + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $inUseId = 0; + foreach ($values as $value) { + if ($value['value'] === 'SYS_INVENTORY_FILTER_IN_USE_ITEMS') { + $inUseId = $value['id']; + break; + } + } + $newItem = new Item($this->mDb, $this, 0); $newItem->setValue('ini_org_id', $this->organizationId); - $newItem->setValue('ini_retired', 0); + $newItem->setValue('ini_status', $inUseId); $newItem->setValue('ini_cat_id', $category->getValue('cat_id')); $newItem->save(); @@ -958,8 +1037,19 @@ public function deleteItem(): void */ public function retireItem(): void { + // get the option id of the retired status + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $retiredId = 0; + foreach ($values as $value) { + if ($value['value'] === 'SYS_INVENTORY_FILTER_RETIRED_ITEMS') { + $retiredId = $value['id']; + break; + } + } + $item = new Item($this->mDb, $this, $this->mItemId); - $item->setValue('ini_retired', 1); + $item->setValue('ini_status', $retiredId); $item->save(); $this->mItemRetired = true; @@ -974,8 +1064,18 @@ public function retireItem(): void */ public function reinstateItem(): void { + // get the option id of the in use status + $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $inUseId = 0; + foreach ($values as $value) { + if ($value['value'] === 'SYS_INVENTORY_FILTER_IN_USE_ITEMS') { + $inUseId = $value['id']; + break; + } + } $item = new Item($this->mDb, $this, $this->mItemId); - $item->setValue('ini_retired', 0); + $item->setValue('ini_status', $inUseId); $item->save(); $this->mItemRetired = false; @@ -1010,6 +1110,12 @@ public function saveItemData(): void $item->save(); $value->delete(); } + elseif ($value instanceof ItemData && $value->getValue('ind_inf_id') === 8) { + $item = new Item($this->mDb, $this, $this->mItemId); + $item->setValue('ini_status', $value->getValue('ind_value')); + $item->save(); + $value->delete(); + } elseif ($value instanceof ItemData) { // if value exists and new value is empty then delete entry if ($value->getValue('ind_id') > 0 && $value->getValue('ind_value') === '') { diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index 834e23c62b..02f6b7db91 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -11,6 +11,7 @@ use Admidio\UI\Presenter\FormPresenter; use Admidio\UI\Presenter\PagePresenter; use Admidio\Changelog\Service\ChangelogService; +use Admidio\Infrastructure\Language; /** * @brief Class with methods to display the module pages. @@ -152,7 +153,10 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName } $options = new SelectOptions($gDb, $itemField->getValue('inf_id')); - $optionValueList = $options->getAllOptions($gSettingsManager->getBool('inventory_show_obsolete_select_field_options')); + foreach ($options->getAllOptions($gSettingsManager->getBool('inventory_show_obsolete_select_field_options')) as $option) { + $option['value'] = Language::translateIfTranslationStrId($option['value']); + $optionValueList[] = $option; + } if (empty($optionValueList)) { $optionValueList = array( 0 => array('id' => 1, 'value' => '', 'system' => false, 'sequence' => 0, 'obsolete' => false) diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 12cf14fa5e..4d0f8aef1e 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -3,13 +3,12 @@ namespace Admidio\UI\Presenter; // Admidio namespaces -use Admidio\Categories\Entity\Category; use Admidio\Categories\Service\CategoryService; -use Admidio\Infrastructure\Language; use Admidio\Infrastructure\Exception; use Admidio\Infrastructure\Utils\SecurityUtils; use Admidio\Infrastructure\Utils\StringUtils; use Admidio\Inventory\ValueObjects\ItemsData; +use Admidio\Inventory\Entity\SelectOptions; use Admidio\Changelog\Service\ChangelogService; use Admidio\UI\Component\DataTables; use Admidio\UI\Presenter\FormPresenter; @@ -77,11 +76,12 @@ public function __construct() $this->getFilterString = admFuncVariableIsValid($_GET, 'items_filter_string', 'string', array('defaultValue' => '')); $this->getFilterCategoryUUID = admFuncVariableIsValid($_GET, 'items_filter_category', 'string', array('defaultValue' => '')); $this->getFilterKeeper = admFuncVariableIsValid($_GET, 'items_filter_keeper', 'int', array('defaultValue' => 0)); - $this->getFilterItems = admFuncVariableIsValid($_GET, 'items_filter', 'int', array('defaultValue' => 0)); + $this->getFilterItems = admFuncVariableIsValid($_GET, 'items_filter', 'int', array('defaultValue' => 1)); $this->itemsData = new ItemsData($gDb, $gCurrentOrgId); - $this->showRetiredItems = ($this->getFilterItems >= 1) ? true : false; + // check if the user has selected to show retired items + $this->showRetiredItems = ($this->getFilterItems === 0 || $this->getFilterItems === 2) ? true : false; $this->itemsData->showRetiredItems($this->showRetiredItems); $this->itemsData->readItems(); @@ -279,11 +279,24 @@ protected function createHeader(): void ) ); - $selectBoxValues = array( - '0' => $gL10n->get('SYS_INVENTORY_FILTER_IN_USE_ITEMS'), - '1' => $gL10n->get('SYS_INVENTORY_FILTER_RETIRED_ITEMS'), - '2' => $gL10n->get('SYS_ALL') - ); + // get the status options for the filter +/* $sql = 'SELECT ifo_id, ifo_value + FROM ' . TBL_INVENTORY_FIELD_OPTIONS . ' + WHERE ifo_inf_id = ?'; + $countFilteredStatement = $gDb->queryPrepared($sql, array($this->itemsData->getProperty('STATUS', 'inf_id'))); + $selectBoxValues = array(); + while ($row = $countFilteredStatement->fetch()) { + $selectBoxValues[$row['ifo_id']] = $row['ifo_value']; + } */ + $option = new SelectOptions($gDb, $this->itemsData->getProperty('STATUS', 'inf_id')); + $values = $option->getAllOptions(); + $selectBoxValues = array(); + foreach ($values as $value) { + $selectBoxValues[$value['id']] = $value['value']; + } + // add select all items to select box values + $selectBoxValues[0] = $gL10n->get('SYS_ALL'); + // filter all items $form->addSelectBox( 'items_filter', @@ -708,8 +721,8 @@ public function prepareData(string $mode = 'html') : array ); // Set default alignment and headers for the first column (abbreviation) - $columnAlign[] = 'center'; - $headers = array(0 => ''); + ($mode === 'html') ? $columnAlign[] = 'center' : $columnAlign = array(); + $headers = ($mode === 'html') ? array(0 => '') : array(); $exportHeaders = array(); $columnNumber = 1; //array with the internal field names of the borrowing fields @@ -779,7 +792,7 @@ public function prepareData(string $mode = 'html') : array $this->itemsData->readItemData($item['ini_uuid']); $rowValues = array(); $rowValues['item_uuid'] = $item['ini_uuid']; - $strikethrough = $item['ini_retired']; + $strikethrough = $this->itemsData->isRetired(); $columnNumber = 1; foreach ($this->itemsData->getItemFields() as $itemField) { @@ -793,15 +806,16 @@ public function prepareData(string $mode = 'html') : array if ( ($this->getFilterCategoryUUID !== '' && $infNameIntern === 'CATEGORY' && $this->getFilterCategoryUUID != $this->itemsData->getValue($infNameIntern, 'database')) || ($this->getFilterKeeper !== 0 && $infNameIntern === 'KEEPER' && $this->getFilterKeeper != $this->itemsData->getValue($infNameIntern)) || - ($this->getFilterItems === 0 && $item['ini_retired']) || - ($this->getFilterItems === 1 && !$item['ini_retired']) + ($this->getFilterItems !== 0 && $this->getFilterItems !== $this->itemsData->getStatus()) ) { // skip to the next iteration of the next-outer loop continue 2; } if ($columnNumber === 1) { - $rowValues['data'][] = ''; + if ($mode === 'html') { + $rowValues['data'][] = ''; + } $rowValues['data'][] = $listRowNumber; } @@ -810,8 +824,8 @@ public function prepareData(string $mode = 'html') : array // Process ITEMNAME column if ($infNameIntern === 'ITEMNAME' && strlen($content) > 0) { - if ($mode === 'html' && (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_retired'])) { - $content = '' . SecurityUtils::encodeHTML($content) . ''; + if ($mode === 'html' && (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$this->itemsData->isRetired())) { + $content = '' . SecurityUtils::encodeHTML($content) . ''; } else { $content = SecurityUtils::encodeHTML($content); } @@ -905,16 +919,16 @@ public function prepareData(string $mode = 'html') : array } if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$item['ini_retired']) { + if (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) && !$this->itemsData->isRetired()) { // Add edit action $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), 'icon' => 'bi bi-pencil-square', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); // Add borrow action - if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { + if (!$this->itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { $item_borrowed = false; @@ -941,11 +955,11 @@ public function prepareData(string $mode = 'html') : array ); } - if ($item['ini_retired']) { + if ($this->itemsData->isRetired()) { $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); // Add reinstate action $rowValues['actions'][] = array( - 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', + 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', 'dataMessage' => $dataMessage, 'icon' => 'bi bi-eye', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE') @@ -953,7 +967,7 @@ public function prepareData(string $mode = 'html') : array } if ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (!$item['ini_retired']) { + if (!$this->itemsData->isRetired()) { // Addretire action $rowValues['actions'][] = array( 'popup' => true, @@ -967,7 +981,7 @@ public function prepareData(string $mode = 'html') : array // Add delete/retire action $rowValues['actions'][] = array( 'popup' => true, - 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), + 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), 'icon' => 'bi bi-trash', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_DELETE') ); @@ -1133,7 +1147,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $itemsData->readItemData($item['ini_uuid']); $rowValues = array(); $rowValues['item_uuid'] = $item['ini_uuid']; - $strikethrough = $item['ini_retired']; + $strikethrough = $this->itemsData->isRetired(); $columnNumber = 1; foreach ($itemsData->getItemFields() as $itemField) { @@ -1204,16 +1218,16 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database')) && !$item['ini_retired'])) { + if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database')) && !$this->itemsData->isRetired())) { // Add edit action $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), 'icon' => 'bi bi-pencil-square', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); // Add lend action - if (!$item['ini_retired'] && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { + if (!$this->itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { $item_borrowed = false; @@ -1240,11 +1254,11 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter ); } - if ($item['ini_retired']) { + if ($this->itemsData->isRetired()) { $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); // Add reinstate action $rowValues['actions'][] = array( - 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', + 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', 'dataMessage' => $dataMessage, 'icon' => 'bi bi-eye', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE') @@ -1252,7 +1266,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } if ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (!$item['ini_retired']) { + if (!$this->itemsData->isRetired()) { // Add retire action $rowValues['actions'][] = array( 'popup' => true, @@ -1266,7 +1280,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter // Add delete/retire action $rowValues['actions'][] = array( 'popup' => true, - 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $item['ini_retired'])), + 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), 'icon' => 'bi bi-trash', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_DELETE') ); diff --git a/themes/simple/templates/modules/inventory.list.tpl b/themes/simple/templates/modules/inventory.list.tpl index 24a3109f36..2980f245dc 100644 --- a/themes/simple/templates/modules/inventory.list.tpl +++ b/themes/simple/templates/modules/inventory.list.tpl @@ -1,18 +1,20 @@
-
- -
+ {if !$print} +
+ +
+ {/if} From bf647804bc946ea4818b75a06dc6fab35e99632e Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Thu, 31 Jul 2025 22:04:07 +0200 Subject: [PATCH 06/25] fix: selectOptions Tables --- install/db_scripts/db.sql | 59 +++++++++++++++------- install/db_scripts/update_5_0.xml | 1 + src/ProfileFields/Entity/SelectOptions.php | 4 +- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/install/db_scripts/db.sql b/install/db_scripts/db.sql index 1cb1774c0c..2aea2cc2aa 100644 --- a/install/db_scripts/db.sql +++ b/install/db_scripts/db.sql @@ -916,6 +916,23 @@ COLLATE = utf8_unicode_ci; CREATE UNIQUE INDEX %PREFIX%_idx_usf_name_intern ON %PREFIX%_user_fields (usf_name_intern); CREATE UNIQUE INDEX %PREFIX%_idx_usf_uuid ON %PREFIX%_user_fields (usf_uuid); +/*==============================================================*/ +/* Table: adm_user_field_select_options */ +/*==============================================================*/ +CREATE TABLE %PREFIX%_user_field_select_options +( + ufo_id integer unsigned NOT NULL AUTO_INCREMENT, + ufo_usf_id integer unsigned NOT NULL, -- Connected user field id + ufo_value varchar(255) NOT NULL, -- option value + ufo_system boolean NOT NULL DEFAULT false, -- If true, the option is a system option an not editable + ufo_sequence smallint NOT NULL, -- Position in the list + ufo_obsolete boolean NOT NULL DEFAULT false, -- If true, the option is not available for new entries, but still exists in the database + PRIMARY KEY (ufo_id) +) +ENGINE = InnoDB +DEFAULT character SET = utf8 +COLLATE = utf8_unicode_ci; + /*==============================================================*/ /* Table: adm_user_data */ /*==============================================================*/ @@ -1042,6 +1059,23 @@ COLLATE = utf8_unicode_ci; CREATE UNIQUE INDEX %PREFIX%_idx_inf_name_intern ON %PREFIX%_inventory_fields (inf_org_id, inf_name_intern); CREATE UNIQUE INDEX %PREFIX%_idx_inf_uuid ON %PREFIX%_inventory_fields (inf_uuid); +/*==============================================================*/ +/* Table: adm_inventory_field_select_options */ +/*==============================================================*/ +CREATE TABLE %PREFIX%_inventory_field_select_options +( + ifo_id integer unsigned NOT NULL AUTO_INCREMENT, + ifo_inf_id integer unsigned NOT NULL, -- Connected inventory field id + ifo_value varchar(255) NOT NULL, -- option value + ifo_system boolean NOT NULL DEFAULT false, -- If true, the option is a system option an not editable + ifo_sequence smallint NOT NULL, -- Position in the list + ifo_obsolete boolean NOT NULL DEFAULT false, -- If true, the option is not available for new entries, but still exists in the database + PRIMARY KEY (ifo_id) +) +ENGINE = InnoDB +DEFAULT character SET = utf8 +COLLATE = utf8_unicode_ci; + /*==============================================================*/ /* Table: adm_inventory_item_data */ /*==============================================================*/ @@ -1143,22 +1177,6 @@ ENGINE = InnoDB DEFAULT character SET = utf8 COLLATE = utf8_unicode_ci; -/*==============================================================*/ -/* Table: adm_user_field_select_options */ -/*==============================================================*/ -CREATE TABLE %PREFIX%_user_field_select_options -( - ufo_id integer unsigned NOT NULL AUTO_INCREMENT, - ufo_usf_id integer unsigned NOT NULL, -- Connected user field id - ufo_value varchar(255) NOT NULL, -- option value - ufo_sequence smallint NOT NULL, -- Position in the list - ufo_obsolete boolean NOT NULL DEFAULT false, -- If true, the option is not available for new entries, but still exists in the database - PRIMARY KEY (ufo_id) -) -ENGINE = InnoDB -DEFAULT character SET = utf8 -COLLATE = utf8_unicode_ci; - /*==============================================================*/ /* Foreign Key Constraints */ /*==============================================================*/ @@ -1325,6 +1343,9 @@ ALTER TABLE %PREFIX%_user_fields ADD CONSTRAINT %PREFIX%_fk_usf_usr_create FOREIGN KEY (usf_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_usf_usr_change FOREIGN KEY (usf_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT; +ALTER TABLE %PREFIX%_user_field_select_options + ADD CONSTRAINT %PREFIX%_fk_ufo_usf FOREIGN KEY (ufo_usf_id) REFERENCES %PREFIX%_user_fields (usf_id) ON DELETE RESTRICT ON UPDATE RESTRICT; + ALTER TABLE %PREFIX%_user_data ADD CONSTRAINT %PREFIX%_fk_usd_usf FOREIGN KEY (usd_usf_id) REFERENCES %PREFIX%_user_fields (usf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_usd_usr FOREIGN KEY (usd_usr_id) REFERENCES %PREFIX%_users (usr_id) ON DELETE RESTRICT ON UPDATE RESTRICT; @@ -1345,14 +1366,14 @@ ALTER TABLE %PREFIX%_user_relations ADD CONSTRAINT %PREFIX%_fk_ure_usr_change FOREIGN KEY (ure_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ure_usr_create FOREIGN KEY (ure_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT; -ALTER TABLE %PREFIX%_user_field_select_options - ADD CONSTRAINT %PREFIX%_fk_ufo_usf FOREIGN KEY (ufo_usf_id) REFERENCES %PREFIX%_user_fields (usf_id) ON DELETE RESTRICT ON UPDATE RESTRICT; - ALTER TABLE %PREFIX%_inventory_fields ADD CONSTRAINT %PREFIX%_fk_inf_org FOREIGN KEY (inf_org_id) REFERENCES %PREFIX%_organizations (org_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_inf_usr_create FOREIGN KEY (inf_usr_id_create) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_inf_usr_change FOREIGN KEY (inf_usr_id_change) REFERENCES %PREFIX%_users (usr_id) ON DELETE SET NULL ON UPDATE RESTRICT; +ALTER TABLE %PREFIX%_inventory_field_select_options + ADD CONSTRAINT %PREFIX%_fk_ifo_inf FOREIGN KEY (ifo_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE CASCADE ON UPDATE RESTRICT; + ALTER TABLE %PREFIX%_inventory_item_data ADD CONSTRAINT %PREFIX%_fk_ind_inf FOREIGN KEY (ind_inf_id) REFERENCES %PREFIX%_inventory_fields (inf_id) ON DELETE RESTRICT ON UPDATE RESTRICT, ADD CONSTRAINT %PREFIX%_fk_ind_ini FOREIGN KEY (ind_ini_id) REFERENCES %PREFIX%_inventory_items (ini_id) ON DELETE RESTRICT ON UPDATE RESTRICT; diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index 7771ad4e25..2713f02b00 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -391,6 +391,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ufo_id integer unsigned NOT NULL AUTO_INCREMENT, ufo_usf_id integer unsigned NOT NULL, ufo_value varchar(255) NOT NULL, + ufo_system boolean NOT NULL DEFAULT false, ufo_sequence smallint NOT NULL, ufo_obsolete boolean NOT NULL DEFAULT false, PRIMARY KEY (ufo_id) diff --git a/src/ProfileFields/Entity/SelectOptions.php b/src/ProfileFields/Entity/SelectOptions.php index 00380540c2..1dbf751a0c 100644 --- a/src/ProfileFields/Entity/SelectOptions.php +++ b/src/ProfileFields/Entity/SelectOptions.php @@ -55,7 +55,7 @@ public function readDataByFieldId(int $usfId = 0) : void // if id is set than read the data of the recordset if ($this->usfId > 0) { - $sql = 'SELECT ufo_id, ufo_value, ifo_system, ufo_sequence, ufo_obsolete + $sql = 'SELECT ufo_id, ufo_value, ufo_system, ufo_sequence, ufo_obsolete FROM ' . TBL_USER_FIELD_OPTIONS . ' WHERE ufo_usf_id = ? -- $usfId ORDER BY ufo_sequence'; @@ -104,7 +104,7 @@ public function getAllOptions(bool $withObsoleteEnries = true) : array $values[$value['ufo_id']] = array( 'id' => $value['ufo_id'], 'value' => $value['ufo_value'], - 'system' => $value['ifo_system'], + 'system' => $value['ufo_system'], 'sequence' => $value['ufo_sequence'], 'obsolete' => $value['ufo_obsolete'] ); From d1a353e177ab33d695249f1d110c72c62539d4cd Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 09:55:18 +0200 Subject: [PATCH 07/25] fix: log actual status text instead of option id --- src/Inventory/Entity/Item.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Inventory/Entity/Item.php b/src/Inventory/Entity/Item.php index 8be042cdd0..d6addb0da6 100644 --- a/src/Inventory/Entity/Item.php +++ b/src/Inventory/Entity/Item.php @@ -9,6 +9,7 @@ use Admidio\Inventory\Entity\ItemData; use Admidio\Inventory\Entity\SelectOptions; use Admidio\Changelog\Entity\LogChanges; +use Admidio\Infrastructure\Language; /** * @brief Class manages access to database table adm_files @@ -149,7 +150,7 @@ public function isRetired(): bool { global $gDb; $optionId = $this->getStatus(); - $option = new SelectOptions($gDb); + $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_RETIRED'; } @@ -164,7 +165,7 @@ public function isInUse(): bool { global $gDb; $optionId = $this->getStatus(); - $option = new SelectOptions($gDb); + $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_IN_USE'; } @@ -194,7 +195,7 @@ public function getIgnoredLogColumns(): array * @throws Exception */ protected function adjustLogEntry(LogChanges $logEntry): void - { + { $itemName = $this->mItemsData->getValue('ITEMNAME', 'database'); if (isset($_POST['INF-ITEMNAME']) && $itemName === '') { $itemName = $_POST['INF-ITEMNAME']; @@ -202,6 +203,17 @@ protected function adjustLogEntry(LogChanges $logEntry): void elseif (!isset( $_POST['INF-ITEMNAME']) && $itemName === '') { $itemName = $logEntry->getValue('log_record_name'); } + + // If the item status is changed convert the status id to the actual status text + if ($logEntry->getValue('log_field') === 'ini_status') { + global $gDb; + $itemStatusId = $logEntry->getValue('log_value_new'); + $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); + if ($option->readDataById($itemStatusId)) { + $logEntry->setValue('log_value_new', Language::translateIfTranslationStrId($option->getValue('ifo_value'))); + } + } + $logEntry->setValue('log_record_name', $itemName); $logEntry->setValue('log_related_id', $logEntry->getValue('log_record_id')); } From e56a6ae9d5960620c76cadb0250f2084c93a3907 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 10:08:19 +0200 Subject: [PATCH 08/25] small fixes --- src/Inventory/Service/ExportService.php | 2 +- src/UI/Presenter/FormPresenter.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Inventory/Service/ExportService.php b/src/Inventory/Service/ExportService.php index 40f7f7fd03..1f800176ff 100644 --- a/src/Inventory/Service/ExportService.php +++ b/src/Inventory/Service/ExportService.php @@ -52,7 +52,7 @@ public function createExport(string $mode = 'pdf'): void [$exportMode, $charset, $orientation] = $modeSettings[$mode]; } - $filename = $gSettingsManager->getString('inventory_export_filename'); + $filename = $gSettingsManager->getString('inventory_export_filename') . '.' . $exportMode; if ($gSettingsManager->getBool('inventory_add_date')) { // add system date format to filename $filename = date('Y-m-d') . '_' . $filename; diff --git a/src/UI/Presenter/FormPresenter.php b/src/UI/Presenter/FormPresenter.php index ab7599fcb2..eb2b8a5592 100644 --- a/src/UI/Presenter/FormPresenter.php +++ b/src/UI/Presenter/FormPresenter.php @@ -1054,7 +1054,8 @@ function addOptionRow(dataId, checkUrl, deleteUrl, csrfToken, translationStrings const table = document.querySelector("#" + dataId + "_table tbody.admidio-sortable"); const newRow = document.createElement("tr"); const rows = table.querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\'); - let maxId = document.querySelector("#" + dataId + "_table tbody.admidio-not-sortable").querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length; + let notSortableContainer = document.querySelector("#" + dataId + "_table tbody.admidio-not-sortable"); + let maxId = notSortableContainer ? notSortableContainer.querySelectorAll(\'tr[id^="\' + dataId + \'_option_"]\').length : 0; rows.forEach(row => { const currentId = row.id.replace(dataId + "_option_", ""); const num = parseInt(currentId, 10); From 01f0fbe1b9917e9f66453ad0762a600f37e3ba30 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 11:31:10 +0200 Subject: [PATCH 09/25] fix: import items --- src/Inventory/Entity/ItemField.php | 2 +- src/Inventory/Service/ImportService.php | 43 +++++++++++++++---- src/Inventory/ValueObjects/ItemsData.php | 19 +++++++- src/UI/Presenter/InventoryFieldsPresenter.php | 2 +- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Inventory/Entity/ItemField.php b/src/Inventory/Entity/ItemField.php index 0ece3402f3..dc4a77c443 100644 --- a/src/Inventory/Entity/ItemField.php +++ b/src/Inventory/Entity/ItemField.php @@ -135,7 +135,7 @@ public function getValue($fieldNameIntern, $format = '', bool $withObsoleteEnrie if (!isset($this->dbColumns['inf_description'])) { $value = ''; } elseif ($format === 'database') { - $value = html_entity_decode(StringUtils::strStripTags(Language::translateIfTranslationStrId($this->dbColumns['inf_description'])), ENT_QUOTES, 'UTF-8'); + $value = html_entity_decode(StringUtils::strStripTags($this->dbColumns['inf_description']), ENT_QUOTES, 'UTF-8'); } else { $value = Language::translateIfTranslationStrId($this->dbColumns['inf_description']); } diff --git a/src/Inventory/Service/ImportService.php b/src/Inventory/Service/ImportService.php index 522aa48618..25b72dede3 100644 --- a/src/Inventory/Service/ImportService.php +++ b/src/Inventory/Service/ImportService.php @@ -232,11 +232,21 @@ public function importItems(): array $item = new Item($gDb, $items, $items->getItemId()); $catID = $item->getValue('ini_cat_id'); $category = new Category($gDb); - if ($category->readDataById($catID)); + if ($category->readDataById($catID)) { $itemValues[] = array($itemData->getValue('inf_name_intern') => $category->getValue('cat_name')); + } continue; } - + elseif ($itemData->getValue('inf_name_intern') === 'STATUS') { + $item = new Item($gDb, $items, $items->getItemId()); + $itemStatusId = $item->getStatus(); + $option = new SelectOptions($gDb, $itemData->getValue('inf_id')); + if ($option->readDataById($itemStatusId)) { + $itemValues[] = array($itemData->getValue('inf_name_intern') => Language::translateIfTranslationStrId($option->getValue('ifo_value'))); + } + continue; + } + $itemValues[] = array($itemData->getValue('inf_name_intern') => $itemValue); } $itemValues = array_merge_recursive(...$itemValues); @@ -385,13 +395,30 @@ public function importItems(): array } } elseif($imfNameIntern === 'STATUS') { - $option = new SelectOptions($gDb, $infId); - $optionValues = $option->getAllOptions(); + $statusValue = $values[$infId]; $val = ''; - foreach ($optionValues as $optionData) { - if (Language::translateIfTranslationStrId($optionData['value']) === $values[$infId]) { - $val = $optionData['id']; - break; + if ($statusValue !== '') { + // if no status is given, set the default status + $option = new SelectOptions($gDb, $fields->getValue('inf_id')); + $optionValues = $option->getAllOptions(); + foreach ($optionValues as $optionData) { + if (Language::translateIfTranslationStrId($optionData['value']) === $statusValue) { + $val = $optionData['id']; + break; + } + } + if ($val === '') { + $option = new SelectOptions($gDb, $fields->getValue('inf_id')); + $options = $option->getAllOptions(); + $maxId = 0; + foreach ($options as $optionData) { + if ($optionData['id'] > $maxId) { + $maxId = $optionData['id']; + } + } + $newOption[$maxId + 1] = array('value' => $statusValue); + $option->setOptionValues($newOption); + $val = $option->getValue('ifo_id'); } } } diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index fc8b882dcb..ce6478afd8 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -188,6 +188,21 @@ public function readItemData(string $itemUUID = ''): void $this->mItemId = $itemId; $this->mItemUUID = $itemUUID; + // read the values of the item itself + $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEMS . ' + INNER JOIN ' . TBL_INVENTORY_FIELDS . ' + ON inf_name_intern IN ( ?, ? ) + WHERE ini_id = ? + AND inf_org_id = ?;'; + $itemDataStatement = $this->mDb->queryPrepared($sql, array('CATEGORY', 'STATUS', $itemId, $this->organizationId)); + + while ($row = $itemDataStatement->fetch()) { + if (!array_key_exists($row['inf_id'], $this->mItemData)) { + $this->mItemData[$row['inf_id']] = new Item($this->mDb, $this, $itemId); + } + $this->mItemData[$row['inf_id']]->setArray($row); + } + // read all item data $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEM_DATA . ' INNER JOIN ' . TBL_INVENTORY_FIELDS . ' @@ -204,8 +219,10 @@ public function readItemData(string $itemUUID = ''): void // read all item borrow data $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' + INNER JOIN ' . TBL_INVENTORY_FIELDS . ' + ON inf_name_intern IN ( ?, ?, ? ) WHERE inb_ini_id = ?;'; - $itemBorrowStatement = $this->mDb->queryPrepared($sql, array($itemId)); + $itemBorrowStatement = $this->mDb->queryPrepared($sql, array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE', $itemId)); while ($row = $itemBorrowStatement->fetch()) { foreach ($this->getItemFields() as $itemField) { diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index 02f6b7db91..e3c4ccdc37 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -180,7 +180,7 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName $form->addEditor( 'inf_description', $gL10n->get('SYS_DESCRIPTION'), - $itemField->getValue('inf_description'), + $itemField->getValue('inf_description', 'database'), array('toolbar' => 'AdmidioComments')); $form->addSubmitButton( From e78f153c18ad7cb0a80763e44847db2dc76f5e11 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 13:13:35 +0200 Subject: [PATCH 10/25] feat: remove 'IN_INVENTORY' field --- install/db_scripts/preferences.php | 2 +- languages/en.xml | 7 +- .../Service/UpdateStepsCode.php | 7 +- src/Inventory/Entity/ItemBorrowData.php | 2 +- src/Inventory/Service/ImportService.php | 9 +- src/Inventory/ValueObjects/ItemsData.php | 20 ++- src/Organizations/Entity/Organization.php | 4 +- src/UI/Presenter/InventoryFieldsPresenter.php | 4 +- src/UI/Presenter/InventoryImportPresenter.php | 2 +- src/UI/Presenter/InventoryItemPresenter.php | 147 ++++++------------ src/UI/Presenter/InventoryPresenter.php | 8 +- src/UI/Presenter/PreferencesPresenter.php | 2 +- 12 files changed, 85 insertions(+), 129 deletions(-) diff --git a/install/db_scripts/preferences.php b/install/db_scripts/preferences.php index f09ebf891e..84f92c571b 100644 --- a/install/db_scripts/preferences.php +++ b/install/db_scripts/preferences.php @@ -153,7 +153,7 @@ 'inventory_show_obsolete_select_field_options' => '1', 'inventory_system_field_names_editable' => '0', 'inventory_allow_keeper_edit' => '0', - 'inventory_allowed_keeper_edit_fields' => 'IN_INVENTORY,LAST_RECEIVER,BORROW_DATE,RETURN_DATE', + 'inventory_allowed_keeper_edit_fields' => 'LAST_RECEIVER,BORROW_DATE,RETURN_DATE', 'inventory_current_user_default_keeper' => '0', 'inventory_allow_negative_numbers' => '1', 'inventory_decimal_places' => '1', diff --git a/languages/en.xml b/languages/en.xml index 5811e7eaed..7fa9fa15dd 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -777,7 +777,7 @@ Edit permission for the keeper When this option is enabled, the selected item fields can be modified by the item keeper. Otherwise, these fields are read-only and can only be changed by module administrators. (Default: no) Editable fields by the keeper - The item fields selected here can be edited by the item keeper when the option "Edit permission for the keeper" is enabled. All other fields are read-only and can only be changed by module administrators. (Default: "In Inventory", "Last Recipient", "Borrowed on", "Received back on") + The item fields selected here can be edited by the item keeper when the option "Edit permission for the keeper" is enabled. All other fields are read-only and can only be changed by module administrators. (Default: "Last receiver", "Borrowing date", "Return date") The inventory manager module can be completely disabled, made accessible only to logged-in users, or restricted solely to module administrators via this setting. If access is granted only to logged-in users, then the module is hidden from guests. If only module administrators have access, the module is visible only to users with the "manage inventory" permission. (Default: Enabled) Append date When this option is enabled, the current date in the format YYYY-MM-DD will be prefixed to the export file’s name. (Default: no) @@ -785,6 +785,7 @@ If this option is enabled, negative numbers can also be entered in fields of type "Number" or "Decimal number". Otherwise, only positive numbers are allowed. (Default: yes) Borrowing date The borrowing date of the item to the last recipient + The borrowing date of the item must be before the return date! The category of an item Settings for copying a item: Date representation @@ -803,8 +804,6 @@ Here you can import items from a previous export file or your own file. Import items The following columns of the import file are not assigned to any item fields in InventoryManager: - In inventory - Item is in inventory Item successfully copied Copy the item Create a item @@ -823,7 +822,6 @@ Retire the item You can #VAR1_BOLD#. This has the advantage that the data is preserved and you can later always see who has borrowed this item. Item successfully retired - Item retired? Return the item Items Disable borrowing functionality @@ -889,7 +887,6 @@ Current user as default selection If this option is enabled, the current user is used as the default selection for the keeper when creating a new item. (Default: no) User-defined item fields - In this field you can enter the items for the dropdown list or options field. Each line can record one entry for the dropdown list or options field.\n\nFor the item, the text is not stored later; instead, the selected position from the list is saved. Therefore, if you change the text on one line, all items already assigned will immediately receive the new text. However, if you move an entry to a different line, a different entry may be displayed for the item.\n\nFor an options field, an icon can be displayed instead of text. You can specify the name of an icon from the theme folder (e.g.: ok.png), the URL of an external image (e.g.: https://www.example.com/example.jpg), or an icon from the web font “Bootstrap Icons” (#VAR1#https://icons.getbootstrap.com#VAR2#). Optionally, a tooltip for the icon can be set via a vertical bar (e.g.: female.png|female). is #VAR1# from ISO-8859-1 Save original files additionally diff --git a/src/InstallationUpdate/Service/UpdateStepsCode.php b/src/InstallationUpdate/Service/UpdateStepsCode.php index 24b4604eb0..6adc6ece08 100644 --- a/src/InstallationUpdate/Service/UpdateStepsCode.php +++ b/src/InstallationUpdate/Service/UpdateStepsCode.php @@ -88,12 +88,11 @@ public static function updateStep50AddInventoryFields() $arrItemFields = array( array('inf_type' => 'TEXT', 'inf_name_intern' => 'ITEMNAME', 'inf_name' => 'SYS_INVENTORY_ITEMNAME', 'inf_description' => 'SYS_INVENTORY_ITEMNAME_DESC', 'inf_required_input' => 1, 'inf_sequence' => 0), array('inf_type' => 'CATEGORY', 'inf_name_intern' => 'CATEGORY', 'inf_name' => 'SYS_CATEGORY', 'inf_description' => 'SYS_INVENTORY_CATEGORY_DESC', 'inf_required_input' => 1, 'inf_sequence' => 1), - array('inf_type' => 'TEXT', 'inf_name_intern' => 'KEEPER', 'inf_name' => 'SYS_INVENTORY_KEEPER', 'inf_description' => 'SYS_INVENTORY_KEEPER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 2), - array('inf_type' => 'CHECKBOX', 'inf_name_intern' => 'IN_INVENTORY', 'inf_name' => 'SYS_INVENTORY_IN_INVENTORY', 'inf_description' => 'SYS_INVENTORY_IN_INVENTORY_DESC', 'inf_required_input' => 0, 'inf_sequence' => 3), + array('inf_type' => 'DROPDOWN', 'inf_name_intern' => 'STATUS', 'inf_name' => 'SYS_INVENTORY_STATUS', 'inf_description' => 'SYS_INVENTORY_STATUS_DESC', 'inf_required_input' => 1, 'inf_sequence' => 2), + array('inf_type' => 'TEXT', 'inf_name_intern' => 'KEEPER', 'inf_name' => 'SYS_INVENTORY_KEEPER', 'inf_description' => 'SYS_INVENTORY_KEEPER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 3), array('inf_type' => 'TEXT', 'inf_name_intern' => 'LAST_RECEIVER', 'inf_name' => 'SYS_INVENTORY_LAST_RECEIVER', 'inf_description' => 'SYS_INVENTORY_LAST_RECEIVER_DESC', 'inf_required_input' => 0, 'inf_sequence' => 4), array('inf_type' => 'DATE', 'inf_name_intern' => 'BORROW_DATE', 'inf_name' => 'SYS_INVENTORY_BORROW_DATE', 'inf_description' => 'SYS_INVENTORY_BORROW_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 5), - array('inf_type' => 'DATE', 'inf_name_intern' => 'RETURN_DATE', 'inf_name' => 'SYS_INVENTORY_RETURN_DATE', 'inf_description' => 'SYS_INVENTORY_RETURN_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6), - array('inf_type' => 'DROPDOWN', 'inf_name_intern' => 'STATUS', 'inf_name' => 'SYS_INVENTORY_STATUS', 'inf_description' => 'SYS_INVENTORY_STATUS_DESC', 'inf_required_input' => 1, 'inf_sequence' => 7) + array('inf_type' => 'DATE', 'inf_name_intern' => 'RETURN_DATE', 'inf_name' => 'SYS_INVENTORY_RETURN_DATE', 'inf_description' => 'SYS_INVENTORY_RETURN_DATE_DESC', 'inf_required_input' => 0, 'inf_sequence' => 6) ); $sql = 'SELECT org_id, org_shortname FROM ' . TBL_ORGANIZATIONS; diff --git a/src/Inventory/Entity/ItemBorrowData.php b/src/Inventory/Entity/ItemBorrowData.php index 783beba2b2..876baf9470 100644 --- a/src/Inventory/Entity/ItemBorrowData.php +++ b/src/Inventory/Entity/ItemBorrowData.php @@ -35,7 +35,7 @@ class ItemBorrowData extends Entity * Constructor that will create an object of a recordset of the table adm_user_data. * If the id is set than the specific item will be loaded. * @param Database $database Object of the class Database. This should be the default global object **$gDb**. - * @param int $id The id of the item. If 0, an empty object of the table is created. + * @param int $id The id of the entry. If 0, an empty object of the table is created. * @throws Exception */ public function __construct(Database $database, ?ItemsData $itemsData = null, int $id = 0) diff --git a/src/Inventory/Service/ImportService.php b/src/Inventory/Service/ImportService.php index 25b72dede3..cca66b626c 100644 --- a/src/Inventory/Service/ImportService.php +++ b/src/Inventory/Service/ImportService.php @@ -223,8 +223,7 @@ public function importItems(): array foreach ($items->getItemData() as $key => $itemData) { $itemValue = $itemData->getValue('ind_value'); if ($itemData->getValue('inf_name_intern') === 'KEEPER' || $itemData->getValue('inf_name_intern') === 'LAST_RECEIVER' || - $itemData->getValue('inf_name_intern') === 'IN_INVENTORY' || $itemData->getValue('inf_name_intern') === 'BORROW_DATE' || - $itemData->getValue('inf_name_intern') === 'RETURN_DATE') { + $itemData->getValue('inf_name_intern') === 'BORROW_DATE' || $itemData->getValue('inf_name_intern') === 'RETURN_DATE') { continue; } @@ -267,7 +266,7 @@ public function importItems(): array // get all values of the item fields $importedItemData = array(); //array with the internal field names of the borrowing fields - $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); + $borrowingFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); foreach ($assignedFieldColumn as $row => $values) { foreach ($items->getItemFields() as $fields){ @@ -354,7 +353,7 @@ public function importItems(): array } } } - elseif($imfNameIntern === 'BORROWING_DATE' || $imfNameIntern === 'RETURN_DATE') { + elseif($imfNameIntern === 'BORROW_DATE' || $imfNameIntern === 'RETURN_DATE') { $val = $values[$infId]; if ($val !== '') { // date must be formatted @@ -471,7 +470,7 @@ public function importItems(): array private function compareArrays(array $array1, array $array2) : bool { $array1 = array_filter($array1, function($key) { - return $key !== 'KEEPER' && $key !== 'LAST_RECEIVER' && $key !== 'IN_INVENTORY' && $key !== 'BORROW_DATE' && $key !== 'RETURN_DATE'; + return $key !== 'KEEPER' && $key !== 'LAST_RECEIVER' && $key !== 'BORROW_DATE' && $key !== 'RETURN_DATE'; }, ARRAY_FILTER_USE_KEY); foreach ($array1 as $value) { diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index ce6478afd8..ba26aaf59c 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -854,6 +854,20 @@ public function isInUse(): bool return false; } + public function isBorrowed(): bool + { + // get Values of LAST_RECEIVER, BORROW_DATE and RETURN_DATE for current item + $borrowData = new ItemBorrowData($this->mDb, $this); + $borrowData->readDataByColumns(array('inb_ini_id' => $this->mItemId)); + $lastReceiver = $borrowData->getValue('inb_last_receiver'); + $borrowDate = $borrowData->getValue('inb_borrow_date'); + $returnDate = $borrowData->getValue('inb_return_date'); + // if last receiver is set and borrow date is set and return date is not set then item is borrowed + if ($lastReceiver !== '' && $borrowDate !== '' && $returnDate === '') { + return true; + } + return false; + } /** * If the recordset is new and wasn't read from database or was not stored in database * then this method will return true otherwise false @@ -1270,12 +1284,6 @@ public function sendNotification($importData = null): bool isset($users[$value['oldValue']]) ? $users[$value['oldValue']] : $value['oldValue'], isset($users[$value['newValue']]) ? $users[$value['newValue']] : $value['newValue'] ); - } elseif ($key === 'IN_INVENTORY') { - $changes[] = array( - $key, - $value['oldValue'] == 1 ? $gL10n->get('SYS_YES') : ($value['oldValue'] == 0 ? $gL10n->get('SYS_NO') : $value['oldValue']), - $value['newValue'] == 1 ? $gL10n->get('SYS_YES') : ($value['newValue'] == 0 ? $gL10n->get('SYS_NO') : $value['newValue']) - ); } elseif ($options !== '') { $changes[] = array( $key, diff --git a/src/Organizations/Entity/Organization.php b/src/Organizations/Entity/Organization.php index f8fda82019..91dfe7bf07 100644 --- a/src/Organizations/Entity/Organization.php +++ b/src/Organizations/Entity/Organization.php @@ -228,8 +228,8 @@ public function createBasicData(int $userId) (inf_uuid, inf_org_id, inf_type, inf_name_intern, inf_name, inf_description, inf_system, inf_required_input, inf_sequence, inf_usr_id_create, inf_timestamp_create, inf_usr_id_change, inf_timestamp_change) VALUES (?, ?, \'TEXT\', \'ITEMNAME\', \'SYS_INVENTORY_ITEMNAME\', \'SYS_INVENTORY_ITEMNAME_DESC\', 1, 1, 0, ?, ?, NULL, NULL), (?, ?, \'CATEGORY\', \'CATEGORY\', \'SYS_CATEGORY\', \'SYS_INVENTORY_CATEGORY_DESC\', 1, 1, 1, ?, ?, NULL, NULL), - (?, ?, \'TEXT\', \'KEEPER\', \'SYS_INVENTORY_KEEPER\', \'SYS_INVENTORY_KEEPER_DESC\', 1, 0, 2, ?, ?, NULL, NULL), - (?, ?, \'CHECKBOX\', \'IN_INVENTORY\', \'SYS_INVENTORY_IN_INVENTORY\', \'SYS_INVENTORY_IN_INVENTORY_DESC\', 1, 0, 3, ?, ?, NULL, NULL), + (?, ?, \'DROPDOWN\', \'STATUS\', \'SYS_INVENTORY_STATUS\', \'SYS_INVENTORY_STATUS_DESC\', 1, 1, 2, ?, ?, NULL, NULL), + (?, ?, \'TEXT\', \'KEEPER\', \'SYS_INVENTORY_KEEPER\', \'SYS_INVENTORY_KEEPER_DESC\', 1, 0, 3, ?, ?, NULL, NULL), (?, ?, \'TEXT\', \'LAST_RECEIVER\', \'SYS_INVENTORY_LAST_RECEIVER\', \'SYS_INVENTORY_LAST_RECEIVER_DESC\', 1, 0, 4, ?, ?, NULL, NULL), (?, ?, \'DATE\', \'BORROW_DATE\', \'SYS_INVENTORY_BORROW_DATE\', \'SYS_INVENTORY_BORROW_DATE_DESC\', 1, 0, 5, ?, ?, NULL, NULL), (?, ?, \'DATE\', \'RETURN_DATE\', \'SYS_INVENTORY_RETURN_DATE\', \'SYS_INVENTORY_RETURN_DATE_DESC\', 1, 0, 6, ?, ?, NULL, NULL); diff --git a/src/UI/Presenter/InventoryFieldsPresenter.php b/src/UI/Presenter/InventoryFieldsPresenter.php index e3c4ccdc37..129ec8edf5 100644 --- a/src/UI/Presenter/InventoryFieldsPresenter.php +++ b/src/UI/Presenter/InventoryFieldsPresenter.php @@ -166,7 +166,7 @@ public function createEditForm(string $itemFieldUUID = '', string $itemFieldName 'ifo_inf_options', $gL10n->get('SYS_VALUE_LIST'), $optionValueList, - array('helpTextId' => array('SYS_VALUE_LIST_DESC' /* SYS_INVENTORY_VALUE_LIST_DESC */, array('', '')), 'filename' => 'inventory') + array('helpTextId' => array('SYS_VALUE_LIST_DESC', array('', '')), 'filename' => 'inventory') ); $mandatoryFieldValues = array(0 => 'SYS_NO', 1 => 'SYS_YES'); @@ -252,7 +252,7 @@ public function createList() $itemFieldCategoryID = -1; $prevItemFieldCategoryID = -1; //array with the internal field names of the borrowing fields - $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); + $borrowingFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); foreach ($items->getItemFields() as $itemField) { if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($itemField->getValue('inf_name_intern'), $borrowingFieldNames)) { diff --git a/src/UI/Presenter/InventoryImportPresenter.php b/src/UI/Presenter/InventoryImportPresenter.php index bd9209a585..0b9fa8ad5f 100644 --- a/src/UI/Presenter/InventoryImportPresenter.php +++ b/src/UI/Presenter/InventoryImportPresenter.php @@ -269,7 +269,7 @@ public function createAssignFieldsForm(): void }); //array with the internal field names of the borrowing fields - $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); + $borrowingFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); $items = new ItemsData($gDb, $gCurrentOrgId); foreach ($items->getItemFields() as $itemField) { if($gSettingsManager->GetBool('inventory_items_disable_borrowing') && in_array($itemField->getValue('inf_name_intern'), $borrowingFieldNames)) { diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index 368bc37f70..d613e770fa 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -43,7 +43,7 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) { global $gCurrentSession, $gSettingsManager, $gCurrentUser, $gProfileFields, $gL10n, $gCurrentOrgId, $gDb; //array with the internal field names of the borrow fields not used in the edit form - $borrowFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); + $borrowFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -81,15 +81,6 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) $infNameIntern = $itemField->getValue('inf_name_intern'); // Skip borrow fields that are not used in the edit form if (in_array($itemField->getValue('inf_name_intern'), $borrowFieldNames)) { - if ($infNameIntern === 'IN_INVENTORY') { - // we need to add the checkbox for IN_INVENTORY defaulting to true - $form->addInput( - 'INF-' . $infNameIntern, - $items->getProperty($infNameIntern, 'inf_name'), - ($itemUUID === '') ? true : (bool)$items->getValue($infNameIntern), - array('property' => FormPresenter::FIELD_HIDDEN) - ); - } continue; } @@ -305,7 +296,7 @@ public function createEditItemsForm(array $itemUUIDs = array()) // array with the internal field names of the borrow fields not used in the edit form // we also exclude IITEMNAME from the edit form, because it is only used for displaying item values based on the first entry // and it is not wanted to change the item name for multiple items at once - $borrowFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); + $borrowFieldNames = array('ITEMNAME', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -613,7 +604,7 @@ public function createEditBorrowForm(string $itemUUID) { global $gCurrentSession, $gSettingsManager, $gCurrentUser, $gL10n, $gCurrentOrgId, $gDb; //array with the internal field names of the borrow fields not used in the edit form - $borrowFieldNames = array('ITEMNAME', 'IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); + $borrowFieldNames = array('ITEMNAME', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Create user-defined field object $items = new ItemsData($gDb, $gCurrentOrgId); @@ -648,17 +639,14 @@ public function createEditBorrowForm(string $itemUUID) $helpId = ''; $infNameIntern = $itemField->getValue('inf_name_intern'); - if($infNameIntern === 'IN_INVENTORY') { - $ivtInInventory = $infNameIntern; - } - elseif($infNameIntern === 'LAST_RECEIVER') { + if($infNameIntern === 'LAST_RECEIVER') { $ivtLastReceiver = $infNameIntern; } elseif ($infNameIntern === 'BORROW_DATE') { - $ivtReceivedOn = $infNameIntern; + $ivtBorrowDate = $infNameIntern; } elseif ($infNameIntern === 'RETURN_DATE') { - $ivtReceivedBackOn = $infNameIntern; + $ivtReturnDate = $infNameIntern; } // Skip all fields not used in the borrow form @@ -677,32 +665,30 @@ public function createEditBorrowForm(string $itemUUID) if (!$gCurrentUser->isAdministratorInventory() && !in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { $fieldProperty = FormPresenter::FIELD_DISABLED; } - - if (isset($ivtInInventory, $ivtLastReceiver, $ivtReceivedOn, $ivtReceivedBackOn)) { - // Add JavaScript to check the LAST_RECEIVER field and set the required attribute for ivtReceivedOnId and ivtReceivedBackOnId + + if (isset($ivtLastReceiver, $ivtBorrowDate, $ivtReturnDate)) { + // Add JavaScript to check the LAST_RECEIVER field and set the required attribute for ivtBorrowDate and ivtReturnDate $this->addJavascript(' document.addEventListener("DOMContentLoaded", function() { - if (document.querySelector("[id=\'INF-' . $ivtReceivedOn . '_time\']")) { + if (document.querySelector("[id=\'INF-' . $ivtBorrowDate . '_time\']")) { var pDateTime = "true"; } else { var pDateTime = "false"; } - var ivtInInventoryField = document.querySelector("[id=\'INF-' . $ivtInInventory . '\']"); - var ivtInInventoryGroup = document.getElementById("INF-' . $ivtInInventory . '_group"); var ivtLastReceiverField = document.querySelector("[id=\'INF-' . $ivtLastReceiver . '\']"); - var ivtLastReceiverGroup = document.getElementById("INF-' . $ivtLastReceiver . '_group"); - var ivtReceivedOnField = document.querySelector("[id=\'INF-' . $ivtReceivedOn . '\']"); - + var ivtBorrowDateField = document.querySelector("[id=\'INF-' . $ivtBorrowDate . '\']"); + var ivtReturnDateField = document.querySelector("[id=\'INF-' . $ivtReturnDate . '\']"); + if (pDateTime === "true") { - var ivtReceivedOnFieldTime = document.querySelector("[id=\'INF-' . $ivtReceivedOn . '_time\']"); - var ivtReceivedBackOnFieldTime = document.querySelector("[id=\'INF-' . $ivtReceivedBackOn . '_time\']"); + var ivtBorrowDateFieldTime = document.querySelector("[id=\'INF-' . $ivtBorrowDate . '_time\']"); + var ivtReturnDateFieldTime = document.querySelector("[id=\'INF-' . $ivtReturnDate . '_time\']"); } - var ivtReceivedOnGroup = document.getElementById("INF-' . $ivtReceivedOn . '_group"); - var ivtReceivedBackOnField = document.querySelector("[id=\'INF-' . $ivtReceivedBackOn . '\']"); - var ivtReceivedBackOnGroup = document.getElementById("INF-' . $ivtReceivedBackOn . '_group"); - + var ivtLastReceiverGroup = document.getElementById("INF-' . $ivtLastReceiver . '_group"); + var ivtBorrowDateGroup = document.getElementById("INF-' . $ivtBorrowDate . '_group"); + var ivtReturnDateGroup = document.getElementById("INF-' . $ivtReturnDate . '_group"); + function setRequired(field, group, required) { if (required) { field.setAttribute("required", "required"); @@ -712,91 +698,58 @@ function setRequired(field, group, required) { group.classList.remove("admidio-form-group-required"); } } - - window.checkivtInInventory = function() { - var isInInventoryChecked = ivtInInventoryField.checked; + + window.checkItemBorrowState = function() { var lastReceiverValue = ivtLastReceiverField.value; - var receivedBackOnValue = ivtReceivedBackOnField.value; - - setRequired(ivtReceivedOnField, ivtReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - setRequired(ivtReceivedBackOnField, ivtReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - if (pDateTime === "true") { - setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - setRequired(ivtReceivedBackOnFieldTime, ivtReceivedBackOnGroup, isInInventoryChecked && (lastReceiverValue && lastReceiverValue !== "undefined")); - } - - setRequired(ivtLastReceiverField, ivtLastReceiverGroup, !isInInventoryChecked); - setRequired(ivtReceivedOnField, ivtReceivedOnGroup, !isInInventoryChecked); + var borrowDateValue = ivtBorrowDateField.value; + var returnDateValue = ivtReturnDateField.value; + var requiredLastReceiverCheck = borrowDateValue !== "" || returnDateValue !== ""; + var requiredBorrowDateCheck = (lastReceiverValue && lastReceiverValue !== "undefined") || returnDateValue !== ""; + + setRequired(ivtLastReceiverField, ivtLastReceiverGroup, requiredLastReceiverCheck); + setRequired(ivtBorrowDateField, ivtBorrowDateGroup, requiredBorrowDateCheck); if (pDateTime === "true") { - setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, !isInInventoryChecked); + setRequired(ivtBorrowDateFieldTime, ivtBorrowDateGroup, requiredBorrowDateCheck); } - if (!isInInventoryChecked && (lastReceiverValue === "undefined" || !lastReceiverValue)) { - ivtReceivedOnField.value = ""; - if (pDateTime === "true") { - ivtReceivedOnFieldTime.value = ""; - } - } - - if (receivedBackOnValue !== "") { + if (returnDateValue !== "") { setRequired(ivtLastReceiverField, ivtLastReceiverGroup, true); - setRequired(ivtReceivedOnField, ivtReceivedOnGroup, true); + setRequired(ivtBorrowDateField, ivtBorrowDateGroup, true); if (pDateTime === "true") { - setRequired(ivtReceivedOnFieldTime, ivtReceivedOnGroup, true); - setRequired(ivtReceivedBackOnFieldTime, ivtReceivedBackOnGroup, true); + setRequired(ivtBorrowDateFieldTime, ivtBorrowDateGroup, true); + setRequired(ivtReturnDateFieldTime, ivtReturnDateGroup, true); } } - - var previousivtInInventoryState = isInInventoryChecked; - - ivtInInventoryField.addEventListener("change", function() { - if (!ivtInInventoryField.checked && previousivtInInventoryState) { - ivtReceivedBackOnField.value = ""; - if (pDateTime === "true") { - ivtReceivedBackOnFieldTime.value = ""; - } - } - previousivtInInventoryState = ivtInInventoryField.checked; - window.checkivtInInventory(); - }); - - ivtLastReceiverField.addEventListener("change", window.checkivtInInventory); - ivtReceivedBackOnField.addEventListener("input", window.checkivtInInventory); - ivtReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); - if (pDateTime === "true") { - ivtReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); - ivtReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); - } } function validateReceivedOnAndBackOn() { if (pDateTime === "true") { - var receivedOnDate = new Date(ivtReceivedOnField.value + " " + ivtReceivedOnFieldTime.value); - var receivedBackOnDate = new Date(ivtReceivedBackOnField.value + " " + ivtReceivedBackOnFieldTime.value); + var receivedOnDate = new Date(ivtBorrowDateField.value + " " + ivtBorrowDateFieldTime.value); + var receivedBackOnDate = new Date(ivtReturnDateField.value + " " + ivtReturnDateFieldTime.value); } else { - var receivedOnDate = new Date(ivtReceivedOnField.value); - var receivedBackOnDate = new Date(ivtReceivedBackOnField.value); + var receivedOnDate = new Date(ivtBorrowDateField.value); + var receivedBackOnDate = new Date(ivtReturnDateField.value); } if (receivedOnDate > receivedBackOnDate) { - ivtReceivedOnField.setCustomValidity("ReceivedOn date cannot be after ReceivedBack date."); + ivtBorrowDateField.setCustomValidity("' . $gL10n->get('SYS_INVENTORY_BORROW_DATE_WARNING') . '"); } else { - ivtReceivedOnField.setCustomValidity(""); + ivtBorrowDateField.setCustomValidity(""); } } - ivtInInventoryField.addEventListener("change", window.checkivtInInventory); - ivtLastReceiverField.addEventListener("change", window.checkivtInInventory); - - ivtReceivedOnField.addEventListener("input", validateReceivedOnAndBackOn); - ivtReceivedBackOnField.addEventListener("input", window.checkivtInInventory); - + ivtLastReceiverField.addEventListener("change", window.checkItemBorrowState); + ivtBorrowDateField.addEventListener("input", window.checkItemBorrowState); + ivtReturnDateField.addEventListener("input", window.checkItemBorrowState); + + ivtBorrowDateField.addEventListener("input", validateReceivedOnAndBackOn); + ivtReturnDateField.addEventListener("input", validateReceivedOnAndBackOn); if (pDateTime === "true") { - ivtReceivedOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); - ivtReceivedBackOnFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtBorrowDateFieldTime.addEventListener("input", validateReceivedOnAndBackOn); + ivtReturnDateFieldTime.addEventListener("input", validateReceivedOnAndBackOn); } - ivtReceivedBackOnField.addEventListener("input", validateReceivedOnAndBackOn); - window.checkivtInInventory(); + + window.checkItemBorrowState(); }); '); } @@ -875,7 +828,7 @@ function isSelect2Empty(selectId) { // Hole den aktuellen Wert des Select2-Feldes var renderedElement = $("#select2-INF-' . $ivtLastReceiver .'-container"); if (renderedElement.length) { - window.checkivtInInventory(); + window.checkItemBorrowState(); } } // Prüfe, ob der Default-Wert in den Optionen enthalten ist diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 4d0f8aef1e..7a9a6a3d93 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -726,7 +726,7 @@ public function prepareData(string $mode = 'html') : array $exportHeaders = array(); $columnNumber = 1; //array with the internal field names of the borrowing fields - $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); + $borrowingFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // Build headers and column alignment for each item field foreach ($this->itemsData->getItemFields() as $itemField) { @@ -930,7 +930,7 @@ public function prepareData(string $mode = 'html') : array // Add borrow action if (!$this->itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory - if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { + if (!$this->itemsData->isBorrowed()) { $item_borrowed = false; $icon ='bi bi-box-arrow-right'; $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); @@ -1083,7 +1083,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $headers = array(); $columnNumber = 1; //array with the internal field names of the borrow fields - $borrowFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); + $borrowFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); // create array with all column heading values $profileItemFields = array('ITEMNAME'); @@ -1229,7 +1229,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter // Add lend action if (!$this->itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory - if ($this->itemsData->getValue('IN_INVENTORY', 'database') === '1') { + if (!$this->itemsData->isBorrowed()) { $item_borrowed = false; $icon ='bi bi-box-arrow-right'; $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); diff --git a/src/UI/Presenter/PreferencesPresenter.php b/src/UI/Presenter/PreferencesPresenter.php index 5c2b39925f..3bab5722aa 100644 --- a/src/UI/Presenter/PreferencesPresenter.php +++ b/src/UI/Presenter/PreferencesPresenter.php @@ -783,7 +783,7 @@ public function createInventoryForm(): string global $gL10n, $gSettingsManager, $gDb, $gCurrentOrgId, $gCurrentSession, $gCurrentUser; $formValues = $gSettingsManager->getAll(); //array with the internal field names of the borrowing fields - $borrowingFieldNames = array('IN_INVENTORY', 'LAST_RECEIVER', 'BORROWING_DATE', 'RETURN_DATE'); + $borrowingFieldNames = array('LAST_RECEIVER', 'BORROW_DATE', 'RETURN_DATE'); $formInventory = new FormPresenter( 'adm_preferences_form_inventory', From 77192c57f8ccd3e62b988cad374693e5664e9d4d Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 13:14:16 +0200 Subject: [PATCH 11/25] fix: correct spelling of 'separator' in form methods and templates --- src/UI/Presenter/FormPresenter.php | 4 ++-- src/UI/Presenter/PreferencesPresenter.php | 12 ++++++------ .../templates/preferences/preferences.inventory.tpl | 6 +++--- .../{form.seperator.tpl => form.separator.tpl} | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) rename themes/simple/templates/sys-template-parts/{form.seperator.tpl => form.separator.tpl} (90%) diff --git a/src/UI/Presenter/FormPresenter.php b/src/UI/Presenter/FormPresenter.php index eb2b8a5592..f00c28e6d3 100644 --- a/src/UI/Presenter/FormPresenter.php +++ b/src/UI/Presenter/FormPresenter.php @@ -465,10 +465,10 @@ public function addDescription(string $id, string $content, array $options = arr * If you don't need the field structure and want to add html then use the method addHtml() * @param string $label The label of the custom content. */ - public function addSeperator(string $id, string $label = '', array $options = array()): void + public function addSeparator(string $id, string $label = '', array $options = array()): void { $optionsAll = $this->buildOptionsArray(array_replace(array( - 'type' => 'seperator', + 'type' => 'separator', 'id' => $id, 'label' => $label ), $options)); diff --git a/src/UI/Presenter/PreferencesPresenter.php b/src/UI/Presenter/PreferencesPresenter.php index 3bab5722aa..2321e2ef72 100644 --- a/src/UI/Presenter/PreferencesPresenter.php +++ b/src/UI/Presenter/PreferencesPresenter.php @@ -823,8 +823,8 @@ public function createInventoryForm(): string ); // general settings - $formInventory->addSeperator( - 'inventory_seperator_general_settings', + $formInventory->addSeparator( + 'inventory_separator_general_settings', $gL10n->get('SYS_COMMON') ); @@ -906,8 +906,8 @@ public function createInventoryForm(): string ); // profile view settings - $formInventory->addSeperator( - 'inventory_seperator_profile_view_settings', + $formInventory->addSeparator( + 'inventory_separator_profile_view_settings', $gL10n->get('SYS_INVENTORY_PROFILE_VIEW') ); @@ -935,8 +935,8 @@ public function createInventoryForm(): string ); // export settings - $formInventory->addSeperator( - 'inventory_seperator_export_settings', + $formInventory->addSeparator( + 'inventory_separator_export_settings', $gL10n->get('SYS_INVENTORY_EXPORT') ); diff --git a/themes/simple/templates/preferences/preferences.inventory.tpl b/themes/simple/templates/preferences/preferences.inventory.tpl index d118e48c13..4b7ddbd598 100644 --- a/themes/simple/templates/preferences/preferences.inventory.tpl +++ b/themes/simple/templates/preferences/preferences.inventory.tpl @@ -34,7 +34,7 @@ {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_module_enabled']} {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_items_per_page']} {include 'sys-template-parts/form.input.tpl' data=$elements['inventory_field_history_days']} - {include 'sys-template-parts/form.seperator.tpl' data=$elements['inventory_seperator_general_settings']} + {include 'sys-template-parts/form.separator.tpl' data=$elements['inventory_separator_general_settings']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_show_obsolete_select_field_options']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_items_disable_borrowing']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_system_field_names_editable']} @@ -44,10 +44,10 @@ {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_allow_negative_numbers']} {include 'sys-template-parts/form.input.tpl' data=$elements['inventory_decimal_places']} {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_field_date_time_format']} - {include 'sys-template-parts/form.seperator.tpl' data=$elements['inventory_seperator_profile_view_settings']} + {include 'sys-template-parts/form.separator.tpl' data=$elements['inventory_separator_profile_view_settings']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_profile_view_enabled']} {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_profile_view']} - {include 'sys-template-parts/form.seperator.tpl' data=$elements['inventory_seperator_export_settings']} + {include 'sys-template-parts/form.separator.tpl' data=$elements['inventory_separator_export_settings']} {include 'sys-template-parts/form.input.tpl' data=$elements['inventory_export_filename']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_add_date']} {include 'sys-template-parts/form.button.tpl' data=$elements['adm_button_save_inventory']} diff --git a/themes/simple/templates/sys-template-parts/form.seperator.tpl b/themes/simple/templates/sys-template-parts/form.separator.tpl similarity index 90% rename from themes/simple/templates/sys-template-parts/form.seperator.tpl rename to themes/simple/templates/sys-template-parts/form.separator.tpl index 58288032e6..e2d9b648d1 100644 --- a/themes/simple/templates/sys-template-parts/form.seperator.tpl +++ b/themes/simple/templates/sys-template-parts/form.separator.tpl @@ -1,4 +1,4 @@ -
From 52a2dfb3b0db5589f28e0b4edeea93240fb759a9 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 13:14:27 +0200 Subject: [PATCH 12/25] fix: improve field visibility handling in inventory preferences --- .../preferences/preferences.inventory.tpl | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/themes/simple/templates/preferences/preferences.inventory.tpl b/themes/simple/templates/preferences/preferences.inventory.tpl index 4b7ddbd598..02b6f97117 100644 --- a/themes/simple/templates/preferences/preferences.inventory.tpl +++ b/themes/simple/templates/preferences/preferences.inventory.tpl @@ -2,9 +2,6 @@ /* Function to handle the visibility of fields based on the corresponding option */ $(function(){ /* Keeper edit */ - if(!$("#inventory_allow_keeper_edit").is(":checked")) { - $("#inventory_allowed_keeper_edit_fields_group").slideUp("slow"); - } $("#inventory_allow_keeper_edit").on("change", function() { if(!$("#inventory_allow_keeper_edit").is(":checked")) { $("#inventory_allowed_keeper_edit_fields_group").slideUp("slow"); @@ -13,9 +10,6 @@ } }); /* Profile view */ - if(!$("#inventory_profile_view_enabled").is(":checked")) { - $("#inventory_profile_view_group").slideUp("slow"); - } $("#inventory_profile_view_enabled").on("change", function() { if(!$("#inventory_profile_view_enabled").is(":checked")) { $("#inventory_profile_view_group").slideUp("slow"); @@ -23,6 +17,21 @@ $("#inventory_profile_view_group").slideDown("slow"); } }); + + // wait for the form to be fully visible + var interval = setInterval(function() { + if (!$("#inventory_profile_view_group").is(":hidden") && !$("#inventory_allowed_keeper_edit_fields_group").is(":hidden")) { + clearInterval(interval); + + // now we can initialize the visibility of the fields + if(!$("#inventory_allow_keeper_edit").is(":checked")) { + $("#inventory_allowed_keeper_edit_fields_group").slideUp("slow"); + } + if(!$("#inventory_profile_view_enabled").is(":checked")) { + $("#inventory_profile_view_group").slideUp("slow"); + } + } + }, 100); }); From 696968122f2c2ab602f764b6c1599e5ada2a9bb1 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 16:42:00 +0200 Subject: [PATCH 13/25] fix: update inventory filter parameters for consistency and clarity and fix import of items --- modules/profile/profile.php | 8 ++-- src/Inventory/Entity/Item.php | 4 +- src/Inventory/Service/ImportService.php | 29 ++++-------- src/Inventory/Service/ItemService.php | 8 +--- src/Inventory/ValueObjects/ItemsData.php | 26 +++------- src/UI/Presenter/InventoryPresenter.php | 60 ++++++++++-------------- 6 files changed, 49 insertions(+), 86 deletions(-) diff --git a/modules/profile/profile.php b/modules/profile/profile.php index 07f82853d6..52dd9b3989 100644 --- a/modules/profile/profile.php +++ b/modules/profile/profile.php @@ -440,7 +440,7 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('keeperList', $templateData); $page->assignSmartyVariable('keeperListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsKeeper->getProperty('KEEPER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); } break; @@ -451,7 +451,7 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('receiverList', $templateData); $page->assignSmartyVariable('receiverListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsReceiver->getProperty('LAST_RECEIVER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); } break; @@ -465,13 +465,13 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('keeperList', $templateDataKeeper); $page->assignSmartyVariable('keeperListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsKeeper->getProperty('KEEPER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); } $page->assignSmartyVariable('receiverList', $templateDataReceiver); $page->assignSmartyVariable('receiverListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsReceiver->getProperty('LAST_RECEIVER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); } break; diff --git a/src/Inventory/Entity/Item.php b/src/Inventory/Entity/Item.php index d6addb0da6..af548d544a 100644 --- a/src/Inventory/Entity/Item.php +++ b/src/Inventory/Entity/Item.php @@ -152,7 +152,7 @@ public function isRetired(): bool $optionId = $this->getStatus(); $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { - return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_RETIRED'; + return $option->getValue('ifo_value') === 'SYS_INVENTORY_FILTER_RETIRED_ITEMS'; } return false; } @@ -167,7 +167,7 @@ public function isInUse(): bool $optionId = $this->getStatus(); $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { - return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_IN_USE'; + return $option->getValue('ifo_value') === 'SYS_INVENTORY_FILTER_IN_USE_ITEMS'; } return false; } diff --git a/src/Inventory/Service/ImportService.php b/src/Inventory/Service/ImportService.php index cca66b626c..70d037ab97 100644 --- a/src/Inventory/Service/ImportService.php +++ b/src/Inventory/Service/ImportService.php @@ -226,28 +226,17 @@ public function importItems(): array $itemData->getValue('inf_name_intern') === 'BORROW_DATE' || $itemData->getValue('inf_name_intern') === 'RETURN_DATE') { continue; } - - if ($itemData->getValue('inf_name_intern') === 'CATEGORY') { - $item = new Item($gDb, $items, $items->getItemId()); - $catID = $item->getValue('ini_cat_id'); - $category = new Category($gDb); - if ($category->readDataById($catID)) { - $itemValues[] = array($itemData->getValue('inf_name_intern') => $category->getValue('cat_name')); - } - continue; - } - elseif ($itemData->getValue('inf_name_intern') === 'STATUS') { - $item = new Item($gDb, $items, $items->getItemId()); - $itemStatusId = $item->getStatus(); - $option = new SelectOptions($gDb, $itemData->getValue('inf_id')); - if ($option->readDataById($itemStatusId)) { - $itemValues[] = array($itemData->getValue('inf_name_intern') => Language::translateIfTranslationStrId($option->getValue('ifo_value'))); - } - continue; - } $itemValues[] = array($itemData->getValue('inf_name_intern') => $itemValue); } + // also add a column with the category if it exists + $item = new Item($gDb, $items, $items->getItemId()); + $catID = $item->getValue('ini_cat_id'); + $category = new Category($gDb); + if ($category->readDataById($catID)) { + $itemValues[] = array('CATEGORY' => $category->getValue('cat_name')); + } + $itemValues = array_merge_recursive(...$itemValues); if (count($assignedFieldColumn) === 0) { @@ -336,7 +325,7 @@ public function importItems(): array $categoryService = new CategoryService($gDb, 'IVT'); $allCategories = $categoryService->getVisibleCategories(); foreach ($allCategories as $key => $category) { - if ($category['cat_name'] === $catName) { + if (Language::translateIfTranslationStrId($category['cat_name']) === $catName) { $val = $category['cat_uuid']; break; } diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index e5e90ea9d7..b5753b935c 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -145,14 +145,10 @@ public function save(bool $multiEdit = false): void isset($formValues[$postKey . '_time']) ? $dateValue = $formValues[$postKey] . ' ' . $formValues[$postKey . '_time'] : $dateValue = $formValues[$postKey]; // Write value from field to the item class object with time - if (!$this->itemRessource->setValue($infNameIntern, $dateValue)) { - throw new Exception($gL10n->get('SYS_DATABASE_ERROR'), $gL10n->get('SYS_ERROR')); - } + $this->itemRessource->setValue($infNameIntern, $dateValue); } else { // Write value from field to the item class object - if (!$this->itemRessource->setValue($infNameIntern, $formValues[$postKey])) { - throw new Exception($gL10n->get('SYS_DATABASE_ERROR'), $gL10n->get('SYS_ERROR')); - } + $this->itemRessource->setValue($infNameIntern, $formValues[$postKey]); } } elseif ($itemField->getValue('inf_type') === 'CHECKBOX' && !$multiEdit) { // Set value to '0' for unchecked checkboxes diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index ba26aaf59c..412c2f79e0 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -188,20 +188,6 @@ public function readItemData(string $itemUUID = ''): void $this->mItemId = $itemId; $this->mItemUUID = $itemUUID; - // read the values of the item itself - $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEMS . ' - INNER JOIN ' . TBL_INVENTORY_FIELDS . ' - ON inf_name_intern IN ( ?, ? ) - WHERE ini_id = ? - AND inf_org_id = ?;'; - $itemDataStatement = $this->mDb->queryPrepared($sql, array('CATEGORY', 'STATUS', $itemId, $this->organizationId)); - - while ($row = $itemDataStatement->fetch()) { - if (!array_key_exists($row['inf_id'], $this->mItemData)) { - $this->mItemData[$row['inf_id']] = new Item($this->mDb, $this, $itemId); - } - $this->mItemData[$row['inf_id']]->setArray($row); - } // read all item data $sql = 'SELECT * FROM ' . TBL_INVENTORY_ITEM_DATA . ' @@ -836,9 +822,9 @@ public function isRetired(): bool { global $gDb; $optionId = $this->getStatus(); - $option = new SelectOptions($gDb); + $option = new SelectOptions($gDb, $this->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { - return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_RETIRED'; + return $option->getValue('ifo_value') === 'SYS_INVENTORY_FILTER_RETIRED_ITEMS'; } return false; } @@ -847,9 +833,9 @@ public function isInUse(): bool { global $gDb; $optionId = $this->getStatus(); - $option = new SelectOptions($gDb); + $option = new SelectOptions($gDb, $this->getProperty('STATUS', 'inf_id')); if ($option->readDataById($optionId)) { - return $option->getValue('ifo_value') === 'SYS_INVENTORY_STATUS_IN_USE'; + return $option->getValue('ifo_value') === 'SYS_INVENTORY_FILTER_IN_USE_ITEMS'; } return false; } @@ -1131,7 +1117,7 @@ public function saveItemData(): void } // dont safe CATEGORY field to items data - if ($value instanceof ItemData && $value->getValue('ind_inf_id') === 2) { + if ($value instanceof ItemData && ($value->getValue('ind_inf_id') === 2 || $value->getValue('inf_name_intern') === 'CATEGORY')) { // 2 == CATEGORY field $category = new Category($this->mDb); $category->readDataByUuid($value->getValue('ind_value')); $catID = $category->getValue('cat_id'); @@ -1141,7 +1127,7 @@ public function saveItemData(): void $item->save(); $value->delete(); } - elseif ($value instanceof ItemData && $value->getValue('ind_inf_id') === 8) { + elseif ($value instanceof ItemData && ($value->getValue('ind_inf_id') === 3 || $value->getValue('inf_name_intern') === 'STATUS')) { // 3 == STATUS field $item = new Item($this->mDb, $this, $this->mItemId); $item->setValue('ini_status', $value->getValue('ind_value')); $item->save(); diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 7a9a6a3d93..fa2d276c16 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -55,9 +55,9 @@ class InventoryPresenter extends PagePresenter */ protected int $getFilterKeeper = 0; /** - * @var int filter for all items + * @var int filter id for the status selection */ - protected int $getFilterItems = 0; + protected int $getFilterStatus = 0; /** * @var bool true if all items should be shown */ @@ -76,12 +76,12 @@ public function __construct() $this->getFilterString = admFuncVariableIsValid($_GET, 'items_filter_string', 'string', array('defaultValue' => '')); $this->getFilterCategoryUUID = admFuncVariableIsValid($_GET, 'items_filter_category', 'string', array('defaultValue' => '')); $this->getFilterKeeper = admFuncVariableIsValid($_GET, 'items_filter_keeper', 'int', array('defaultValue' => 0)); - $this->getFilterItems = admFuncVariableIsValid($_GET, 'items_filter', 'int', array('defaultValue' => 1)); + $this->getFilterStatus = admFuncVariableIsValid($_GET, 'items_filter_status', 'int', array('defaultValue' => 1)); $this->itemsData = new ItemsData($gDb, $gCurrentOrgId); // check if the user has selected to show retired items - $this->showRetiredItems = ($this->getFilterItems === 0 || $this->getFilterItems === 2) ? true : false; + $this->showRetiredItems = ($this->getFilterStatus === 0 || $this->getFilterStatus === 2) ? true : false; $this->itemsData->showRetiredItems($this->showRetiredItems); $this->itemsData->readItems(); @@ -158,7 +158,7 @@ protected function createHeader(): void $this->addJavascript(' // only submit non-empty filter values - $("#items_filter_category, #items_filter_keeper, #items_filter").on("change", function(){ + $("#items_filter_category, #items_filter_keeper, #items_filter_status").on("change", function(){ var form = $("#adm_navbar_filter_form"); // Text-Filter @@ -186,11 +186,11 @@ protected function createHeader(): void } // items filter - var itemsSelect = $("#items_filter"); + var itemsSelect = $("#items_filter_status"); if (itemsSelect.val() === "") { itemsSelect.removeAttr("name"); } else { - itemsSelect.attr("name", "items_filter"); + itemsSelect.attr("name", "items_filter_status"); } form.submit(); @@ -218,15 +218,15 @@ protected function createHeader(): void // create the print view link with the current filter values $("#menu_item_lists_print_view").off("click").on("click", function(e){ e.preventDefault(); - var textFilter = $("#items_filter_string").val() || ""; - var category = $("#items_filter_category").val() || ""; - var keeper = $("#items_filter_keeper").val() || ""; - var filterItems = $("#items_filter").val() || ""; + var textFilter = $("#items_filter_string").val() || ""; + var category = $("#items_filter_category").val() || ""; + var keeper = $("#items_filter_keeper").val() || ""; + var filterItems = $("#items_filter_status").val() || ""; var url = "' . $printBaseUrl . '" + "&items_filter_string=" + encodeURIComponent(textFilter) + "&items_filter_category=" + encodeURIComponent(category) + "&items_filter_keeper=" + encodeURIComponent(keeper) - + "&items_filter=" + encodeURIComponent(filterItems); + + "&items_filter_status=" + encodeURIComponent(filterItems); window.open(url, "_blank"); });', @@ -280,14 +280,6 @@ protected function createHeader(): void ); // get the status options for the filter -/* $sql = 'SELECT ifo_id, ifo_value - FROM ' . TBL_INVENTORY_FIELD_OPTIONS . ' - WHERE ifo_inf_id = ?'; - $countFilteredStatement = $gDb->queryPrepared($sql, array($this->itemsData->getProperty('STATUS', 'inf_id'))); - $selectBoxValues = array(); - while ($row = $countFilteredStatement->fetch()) { - $selectBoxValues[$row['ifo_id']] = $row['ifo_value']; - } */ $option = new SelectOptions($gDb, $this->itemsData->getProperty('STATUS', 'inf_id')); $values = $option->getAllOptions(); $selectBoxValues = array(); @@ -299,12 +291,12 @@ protected function createHeader(): void // filter all items $form->addSelectBox( - 'items_filter', + 'items_filter_status', $gL10n->get('SYS_INVENTORY_ITEMS'), $selectBoxValues, array( 'property' => $showFilterForm, - 'defaultValue' => $this->getFilterItems, + 'defaultValue' => $this->getFilterStatus, 'showContextDependentFirstEntry' => false ) ); @@ -336,7 +328,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_xlsx' ) ), @@ -350,7 +342,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_ods' ) ), @@ -364,7 +356,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_csv-ms' ) ), @@ -378,7 +370,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_csv-oo' ) ), @@ -392,7 +384,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_pdf' ) ), @@ -406,7 +398,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, - 'items_filter' => $this->getFilterItems, + 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_pdfl' ) ), @@ -428,16 +420,16 @@ protected function createExportDropdown() : void $.each(buttons, function(suffix, modeValue){ var selector = "#menu_item_lists_" + suffix; $(selector).on("click", function(e){ - var textFilter = $("#items_filter_string").val() || ""; - var category = $("#items_filter_category").val() || ""; - var keeper = $("#items_filter_keeper").val() || ""; - var filterItems = $("#items_filter").val() || ""; + var textFilter = $("#items_filter_string").val() || ""; + var category = $("#items_filter_category").val() || ""; + var keeper = $("#items_filter_keeper").val() || ""; + var filterItems = $("#items_filter_status").val() || ""; var base = this.href.split("?")[0]; var qs = [ "items_filter_string=" + encodeURIComponent(textFilter), "items_filter_category=" + encodeURIComponent(category), "items_filter_keeper=" + encodeURIComponent(keeper), - "items_filter=" + encodeURIComponent(filterItems), + "items_filter_status=" + encodeURIComponent(filterItems), "mode=" + modeValue ].join("&"); this.href = base + "?" + qs; @@ -806,7 +798,7 @@ public function prepareData(string $mode = 'html') : array if ( ($this->getFilterCategoryUUID !== '' && $infNameIntern === 'CATEGORY' && $this->getFilterCategoryUUID != $this->itemsData->getValue($infNameIntern, 'database')) || ($this->getFilterKeeper !== 0 && $infNameIntern === 'KEEPER' && $this->getFilterKeeper != $this->itemsData->getValue($infNameIntern)) || - ($this->getFilterItems !== 0 && $this->getFilterItems !== $this->itemsData->getStatus()) + ($this->getFilterStatus !== 0 && $this->getFilterStatus !== $this->itemsData->getStatus()) ) { // skip to the next iteration of the next-outer loop continue 2; From b0124d1bb4e1e3a5e2a27f81f4708519a3901a01 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 19:37:32 +0200 Subject: [PATCH 14/25] fix: inventory profile view --- modules/profile/profile.php | 10 ++--- src/Inventory/ValueObjects/ItemsData.php | 47 +++++++++++++----------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/modules/profile/profile.php b/modules/profile/profile.php index 52dd9b3989..4b37331aff 100644 --- a/modules/profile/profile.php +++ b/modules/profile/profile.php @@ -397,7 +397,7 @@ function formSubmitEvent(rolesAreaId = "") { if ($gSettingsManager->getInt('inventory_module_enabled') > 0 && $gSettingsManager->getBool('inventory_profile_view_enabled')) { // ****************************************************************************** - // Block with inventory items (optimized) + // Block with inventory items // ****************************************************************************** $itemsKeeper = new ItemsData($gDb, $gCurrentOrgId); $itemsReceiver = new ItemsData($gDb, $gCurrentOrgId); @@ -440,7 +440,7 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('keeperList', $templateData); $page->assignSmartyVariable('keeperListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsKeeper->getProperty('KEEPER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 0, 'items_filter_keeper' => $user->getValue('usr_id')))); } break; @@ -451,7 +451,7 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('receiverList', $templateData); $page->assignSmartyVariable('receiverListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsReceiver->getProperty('LAST_RECEIVER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 0, 'items_filter_last_receiver' => $user->getValue('usr_id')))); } break; @@ -465,13 +465,13 @@ function setupDataTable($page, $tableId, $templateData) $page->assignSmartyVariable('keeperList', $templateDataKeeper); $page->assignSmartyVariable('keeperListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsKeeper->getProperty('KEEPER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryKeeper', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 0, 'items_filter_keeper' => $user->getValue('usr_id')))); } $page->assignSmartyVariable('receiverList', $templateDataReceiver); $page->assignSmartyVariable('receiverListHeader', $gL10n->get('SYS_INVENTORY') . ' (' . $gL10n->get('SYS_VIEW') . ': ' . $itemsReceiver->getProperty('LAST_RECEIVER', 'inf_name') . ')'); if ($gSettingsManager->getInt('inventory_module_enabled') !== 3 || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && $gCurrentUser->isAdministratorInventory())) { - $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 2, 'items_filter_keeper' => $user->getValue('usr_id')))); + $page->assignSmartyVariable('urlInventoryReceiver', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('items_filter_status' => 0, 'items_filter_last_receiver' => $user->getValue('usr_id')))); } break; diff --git a/src/Inventory/ValueObjects/ItemsData.php b/src/Inventory/ValueObjects/ItemsData.php index 412c2f79e0..407c610562 100644 --- a/src/Inventory/ValueObjects/ItemsData.php +++ b/src/Inventory/ValueObjects/ItemsData.php @@ -280,7 +280,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void // first initialize existing data $this->mItems = array(); - $sqlWhereCondition = ''; + $sqlStatusCondition = ''; if (!$this->showRetiredItems) { // get the option id of the retired status $option = new SelectOptions($this->mDb, $this->getProperty('STATUS', 'inf_id')); @@ -292,7 +292,7 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void break; } } - $sqlWhereCondition .= 'AND ini_status = ' . $retiredId; + $sqlStatusCondition .= 'AND ini_status = ' . $retiredId; } $sqlImfIds = 'AND ('; @@ -313,35 +313,38 @@ public function readItemsByUser($userId, $fieldNames = array('KEEPER')): void WHERE (ini_org_id IS NULL OR ini_org_id = ?) AND ind_value = ? - ' . $sqlWhereCondition . ';'; + ' . $sqlStatusCondition . ';'; $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); while ($row = $statement->fetch()) { $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } - // now read the item borrow data for each item - $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_status FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' - INNER JOIN ' . TBL_INVENTORY_ITEMS . ' - ON ini_id = inb_ini_id + // read the borrow data for the given user as receiver + if (in_array('LAST_RECEIVER', $fieldNames)) { + // now read the item borrow data for each item + $sql = 'SELECT DISTINCT ini_id, ini_uuid, ini_cat_id, ini_status FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' + INNER JOIN ' . TBL_INVENTORY_ITEMS . ' + ON ini_id = inb_ini_id WHERE (ini_org_id IS NULL OR ini_org_id = ?) - AND inb_last_receiver = ? - ' . $sqlWhereCondition . ';'; - $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); - // check if a item already exists in the items array - while ($row = $statement->fetch()) { - // check if item already exists in the items array - $itemExists = false; - foreach ($this->mItems as $item) { - if ($item['ini_id'] === $row['ini_id']) { - $itemExists = true; - break; + AND inb_last_receiver = ? + ' . $sqlStatusCondition . ';'; + $statement = $this->mDb->queryPrepared($sql, array($this->organizationId, $userId)); + // check if a item already exists in the items array + while ($row = $statement->fetch()) { + // check if item already exists in the items array + $itemExists = false; + foreach ($this->mItems as $item) { + if ($item['ini_id'] === $row['ini_id']) { + $itemExists = true; + break; + } + } + // if item doesn't exist, then add it to the items array + if (!$itemExists) { + $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } - } - // if item doesn't exist, then add it to the items array - if (!$itemExists) { - $this->mItems[] = array('ini_id' => $row['ini_id'], 'ini_uuid' => $row['ini_uuid'], 'ini_cat_id' => $row['ini_cat_id'], 'ini_status' => $row['ini_status']); } } } From f316914a344bdcb7564264e7a7f0f2c7d0039cfa Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 19:37:58 +0200 Subject: [PATCH 15/25] feat: last receiver filer --- languages/en.xml | 2 +- src/UI/Presenter/InventoryPresenter.php | 86 ++++++++++++++++++++----- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/languages/en.xml b/languages/en.xml index 7fa9fa15dd..2aed4a5656 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -867,7 +867,7 @@ When this option is enabled, the items managed by a member and those borrowed by a member are displayed in the member's profile view. (Default: yes) Property fields to display in the profile view The table shows the items managed by you: - The table shows the items loaned to you: + The table shows the items borrrowed to you: Return date The date the item was returned to the keeper Delete selected items diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index fa2d276c16..7f5d00017c 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -54,6 +54,10 @@ class InventoryPresenter extends PagePresenter * @var int filter id for the keeper selection */ protected int $getFilterKeeper = 0; + /** + * @var bool true if the current user is the keeper of an item + */ + protected string $getFilterLastReceiver = ''; /** * @var int filter id for the status selection */ @@ -76,6 +80,7 @@ public function __construct() $this->getFilterString = admFuncVariableIsValid($_GET, 'items_filter_string', 'string', array('defaultValue' => '')); $this->getFilterCategoryUUID = admFuncVariableIsValid($_GET, 'items_filter_category', 'string', array('defaultValue' => '')); $this->getFilterKeeper = admFuncVariableIsValid($_GET, 'items_filter_keeper', 'int', array('defaultValue' => 0)); + $this->getFilterLastReceiver = admFuncVariableIsValid($_GET, 'items_filter_last_receiver', 'string', array('defaultValue' => '')); $this->getFilterStatus = admFuncVariableIsValid($_GET, 'items_filter_status', 'int', array('defaultValue' => 1)); $this->itemsData = new ItemsData($gDb, $gCurrentOrgId); @@ -158,7 +163,7 @@ protected function createHeader(): void $this->addJavascript(' // only submit non-empty filter values - $("#items_filter_category, #items_filter_keeper, #items_filter_status").on("change", function(){ + $("#items_filter_category, #items_filter_keeper, #items_filter_last_receiver, #items_filter_status").on("change", function(){ var form = $("#adm_navbar_filter_form"); // Text-Filter @@ -185,7 +190,15 @@ protected function createHeader(): void keeperSelect.attr("name", "items_filter_keeper"); } - // items filter + // Last Receiver + var lastReceiverSelect = $("#items_filter_last_receiver"); + if (lastReceiverSelect.val() === "") { + lastReceiverSelect.removeAttr("name"); + } else { + lastReceiverSelect.attr("name", "items_filter_last_receiver"); + } + + // items status filter var itemsSelect = $("#items_filter_status"); if (itemsSelect.val() === "") { itemsSelect.removeAttr("name"); @@ -279,6 +292,42 @@ protected function createHeader(): void ) ); + // get all last receivers + $sql = 'SELECT DISTINCT borrowData.inb_last_receiver, + CASE + WHEN borrowData.inb_last_receiver = \'-1\' + THEN \'n/a\' + WHEN last_name.usd_value IS NOT NULL AND last_name.usd_value <> \'\' AND first_name.usd_value IS NOT NULL AND first_name.usd_value <> \'\' + THEN CONCAT_WS(\', \', last_name.usd_value, first_name.usd_value) + ELSE + borrowData.inb_last_receiver + END AS receiver_name + FROM ' . TBL_INVENTORY_ITEM_BORROW_DATA . ' AS borrowData + INNER JOIN ' . TBL_INVENTORY_FIELDS . ' AS fields + ON fields.inf_name_intern = \'LAST_RECEIVER\' + AND (fields.inf_org_id = ' . $gCurrentOrgId . ' OR fields.inf_org_id IS NULL) + LEFT JOIN ' . TBL_USER_DATA . ' AS last_name + ON last_name.usd_usr_id = borrowData.inb_last_receiver + AND last_name.usd_usf_id = ' . $gProfileFields->getProperty('LAST_NAME','usf_id') . ' + LEFT JOIN ' . TBL_USER_DATA . ' AS first_name + ON first_name.usd_usr_id = borrowData.inb_last_receiver + AND first_name.usd_usf_id = ' . $gProfileFields->getProperty('FIRST_NAME','usf_id') . ' + WHERE fields.inf_name_intern = \'LAST_RECEIVER\' + ORDER BY receiver_name ASC;'; + + // filter last receiver + $form->addSelectBoxFromSql( + 'items_filter_last_receiver', + $gL10n->get('SYS_INVENTORY_LAST_RECEIVER'), + $gDb, + $sql, + array( + 'property' => $showFilterForm, + 'defaultValue' => $this->getFilterLastReceiver, + 'showContextDependentFirstEntry' => true + ) + ); + // get the status options for the filter $option = new SelectOptions($gDb, $this->itemsData->getProperty('STATUS', 'inf_id')); $values = $option->getAllOptions(); @@ -328,6 +377,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_xlsx' ) @@ -342,6 +392,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_ods' ) @@ -356,6 +407,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_csv-ms' ) @@ -370,6 +422,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_csv-oo' ) @@ -384,6 +437,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_pdf' ) @@ -398,6 +452,7 @@ protected function createExportDropdown() : void 'items_filter_string' => $this->getFilterString, 'items_filter_category' => $this->getFilterCategoryUUID, 'items_filter_keeper' => $this->getFilterKeeper, + 'items_filter_last_receiver' => $this->getFilterLastReceiver, 'items_filter_status' => $this->getFilterStatus, 'mode' => 'print_pdfl' ) @@ -798,6 +853,7 @@ public function prepareData(string $mode = 'html') : array if ( ($this->getFilterCategoryUUID !== '' && $infNameIntern === 'CATEGORY' && $this->getFilterCategoryUUID != $this->itemsData->getValue($infNameIntern, 'database')) || ($this->getFilterKeeper !== 0 && $infNameIntern === 'KEEPER' && $this->getFilterKeeper != $this->itemsData->getValue($infNameIntern)) || + ($this->getFilterLastReceiver !== '' && $infNameIntern === 'LAST_RECEIVER' && $this->getFilterLastReceiver != $this->itemsData->getValue($infNameIntern)) || ($this->getFilterStatus !== 0 && $this->getFilterStatus !== $this->itemsData->getStatus()) ) { // skip to the next iteration of the next-outer loop @@ -1122,7 +1178,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } // Append the admin action column - if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { + if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) { $columnAlign[] = 'end'; $headers[] = ' '; } @@ -1139,7 +1195,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $itemsData->readItemData($item['ini_uuid']); $rowValues = array(); $rowValues['item_uuid'] = $item['ini_uuid']; - $strikethrough = $this->itemsData->isRetired(); + $strikethrough = $itemsData->isRetired(); $columnNumber = 1; foreach ($itemsData->getItemFields() as $itemField) { @@ -1209,19 +1265,19 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter $rowValues['actions'][] = $historyButton; } - if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database')) && !$this->itemsData->isRetired())) { + if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) { + if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database')) && !$itemsData->isRetired())) { // Add edit action $rowValues['actions'][] = array( - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $itemsData->isRetired())), 'icon' => 'bi bi-pencil-square', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_EDIT') ); // Add lend action - if (!$this->itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { + if (!$itemsData->isRetired() && !$gSettingsManager->GetBool('inventory_items_disable_borrowing')) { // check if the item is in inventory - if (!$this->itemsData->isBorrowed()) { + if (!$itemsData->isBorrowed()) { $item_borrowed = false; $icon ='bi bi-box-arrow-right'; $tooltip = $gL10n->get('SYS_INVENTORY_ITEM_BORROW'); @@ -1246,19 +1302,19 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter ); } - if ($this->itemsData->isRetired()) { - $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); + if ($itemsData->isRetired()) { + $dataMessage = ($this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) ? $gL10n->get('SYS_INVENTORY_KEEPER_ITEM_REINSTATE_DESC', array('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM')) : $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE_CONFIRM'); // Add reinstate action $rowValues['actions'][] = array( - 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', + 'dataHref' => 'callUrlHideElement(\'adm_inventory_item_' . $item['ini_uuid'] . '\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_reinstate', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $itemsData->isRetired())) . '\', \'' . $gCurrentSession->getCsrfToken() . '\')', 'dataMessage' => $dataMessage, 'icon' => 'bi bi-eye', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_REINSTATE') ); } - if ($this->isKeeperAuthorizedToEdit((int)$this->itemsData->getValue('KEEPER', 'database'))) { - if (!$this->itemsData->isRetired()) { + if ($this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) { + if (!$itemsData->isRetired()) { // Add retire action $rowValues['actions'][] = array( 'popup' => true, @@ -1272,7 +1328,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter // Add delete/retire action $rowValues['actions'][] = array( 'popup' => true, - 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $this->itemsData->isRetired())), + 'dataHref' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_delete_explain_msg', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $itemsData->isRetired())), 'icon' => 'bi bi-trash', 'tooltip' => $gL10n->get('SYS_INVENTORY_ITEM_DELETE') ); From 8201e5f01a75bc8c18c63cab18ee61232779c3d4 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Fri, 1 Aug 2025 19:44:09 +0200 Subject: [PATCH 16/25] fix: actions visibility in profile view --- src/UI/Presenter/InventoryPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 7f5d00017c..1f5d479c69 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -1266,7 +1266,7 @@ public function prepareDataProfile(ItemsData $itemsData, string $itemFieldFilter } if ($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) { - if ($gCurrentUser->isAdministratorInventory() || ($this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database')) && !$itemsData->isRetired())) { + if (($gCurrentUser->isAdministratorInventory() || $this->isKeeperAuthorizedToEdit((int)$itemsData->getValue('KEEPER', 'database'))) && !$itemsData->isRetired()) { // Add edit action $rowValues['actions'][] = array( 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php',array('mode' => 'item_edit', 'item_uuid' => $item['ini_uuid'], 'item_retired' => $itemsData->isRetired())), From 221252f332ba330a550a1619b99c46d5a0536fdc Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 01:35:01 +0200 Subject: [PATCH 17/25] feat: implement item pictures --- install/db_scripts/db.sql | 1 + install/db_scripts/preferences.php | 1 + install/db_scripts/update_5_0.xml | 1 + languages/en.xml | 29 ++- modules/inventory.php | 82 +++++++- src/Changelog/Service/ChangelogService.php | 1 + src/Inventory/Service/ItemService.php | 192 ++++++++++++++++++ src/UI/Presenter/InventoryItemPresenter.php | 92 +++++++++ src/UI/Presenter/InventoryPresenter.php | 14 ++ src/UI/Presenter/PreferencesPresenter.php | 8 + system/js/common_functions.js | 18 +- themes/simple/css/admidio.css | 2 +- .../simple/images/inventory-item-picture.png | Bin 0 -> 167977 bytes .../templates/modules/inventory.item.edit.tpl | 48 +++-- .../modules/inventory.new-item-picture.tpl | 9 + .../inventory.new-item-picture.upload.tpl | 11 + .../preferences/preferences.inventory.tpl | 1 + 17 files changed, 479 insertions(+), 31 deletions(-) create mode 100644 themes/simple/images/inventory-item-picture.png create mode 100644 themes/simple/templates/modules/inventory.new-item-picture.tpl create mode 100644 themes/simple/templates/modules/inventory.new-item-picture.upload.tpl diff --git a/install/db_scripts/db.sql b/install/db_scripts/db.sql index 2aea2cc2aa..8995bbd4ae 100644 --- a/install/db_scripts/db.sql +++ b/install/db_scripts/db.sql @@ -1121,6 +1121,7 @@ CREATE TABLE %PREFIX%_inventory_items ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, ini_status integer unsigned NOT NULL, + ini_picture blob NULL DEFAULT NULL, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, diff --git a/install/db_scripts/preferences.php b/install/db_scripts/preferences.php index 84f92c571b..c13fe78c42 100644 --- a/install/db_scripts/preferences.php +++ b/install/db_scripts/preferences.php @@ -150,6 +150,7 @@ 'inventory_module_enabled' => '2', 'inventory_items_per_page' => '25', 'inventory_field_history_days' => '365', + 'inventory_item_picture_storage' => '0', 'inventory_show_obsolete_select_field_options' => '1', 'inventory_system_field_names_editable' => '0', 'inventory_allow_keeper_edit' => '0', diff --git a/install/db_scripts/update_5_0.xml b/install/db_scripts/update_5_0.xml index 2713f02b00..a4dd3ab6fb 100644 --- a/install/db_scripts/update_5_0.xml +++ b/install/db_scripts/update_5_0.xml @@ -465,6 +465,7 @@ WHERE usf_fn.usf_name_intern = 'FIRST_NAME' AND usf_ln.usf_name_intern = 'LAST_N ini_cat_id integer unsigned NOT NULL, ini_org_id integer unsigned NOT NULL, ini_status integer unsigned NOT NULL, + ini_picture blob NULL DEFAULT NULL, ini_usr_id_create integer unsigned, ini_timestamp_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ini_usr_id_change integer unsigned, diff --git a/languages/en.xml b/languages/en.xml index 2aed4a5656..cf731b93ac 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -804,6 +804,8 @@ Here you can import items from a previous export file or your own file. Import items The following columns of the import file are not assigned to any item fields in InventoryManager: + Borrow the item + Item borrowing data Item successfully copied Copy the item Create a item @@ -814,8 +816,19 @@ Item successfully deleted Change the item Item successfully changed - Borrow the item - Item borrowing data + Item picture + Select item picture + Current item picture + Delete item picture + Item picture successfully deleted + New item picture + The picture may have a maximum resolution of #VAR1# MegaPixels. The picture file must not be larger than #VAR1# MB and must be in JPG or PNG format. + Review new item picture + Item picture successfully saved + Upload item picture + Do you want to delete the item picture? + Storage location of Item pictures + Please define where to store item pictures (in the database or in folder adm_my_files). When a change is made, current pictures are not copied to new location. (default: database) Reinstate the item Do you really want to reinstate the item? Item successfully reinstated @@ -823,12 +836,6 @@ You can #VAR1_BOLD#. This has the advantage that the data is preserved and you can later always see who has borrowed this item. Item successfully retired Return the item - Items - Disable borrowing functionality - When this option is enabled, items can no longer be borrowed. Existing borrowing records will still be saved, but will no longer be displayed. (Default: no) - Change items - The values of the item fields can be changed here.\n\nThe displayed values will be applied to ALL items! - Number of items per page Item field Create new item field Delete the item field @@ -840,6 +847,12 @@ Maintain item fields Item name The name of the item + Items + Disable borrowing functionality + When this option is enabled, items can no longer be borrowed. Existing borrowing records will still be saved, but will no longer be displayed. (Default: no) + Change items + The values of the item fields can be changed here.\n\nThe displayed values will be applied to ALL items! + Number of items per page Keeper Keeper of the item You can mark the item as former.\n\nIf you want to delete the item, please contact an administrator or the manager of the inventory manager!\n\n#VAR1# diff --git a/modules/inventory.php b/modules/inventory.php index 839af43e4e..efa340f5bc 100644 --- a/modules/inventory.php +++ b/modules/inventory.php @@ -10,6 +10,7 @@ use Admidio\Inventory\Service\ImportService; use Admidio\Inventory\Service\ItemFieldService; use Admidio\Inventory\Service\ItemService; +use Admidio\Infrastructure\Utils\FileSystemUtils; use Admidio\UI\Presenter\InventoryFieldsPresenter; use Admidio\UI\Presenter\InventoryImportPresenter; use Admidio\UI\Presenter\InventoryItemPresenter; @@ -39,7 +40,7 @@ require(__DIR__ . '/../system/login_valid.php'); // Initialize and check the parameters - $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_borrow', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_retire', 'item_reinstate', 'item_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); + $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'field_list', 'field_edit', 'field_save', 'field_delete', 'check_option_entry_status', 'delete_option_entry', 'sequence', 'item_edit','item_edit_borrow', 'item_save', 'item_delete_explain_msg', 'item_delete_keeper_explain_msg', 'item_retire', 'item_reinstate', 'item_delete', 'item_picture_show', 'item_picture_show_modal', 'item_picture_choose', 'item_picture_upload', 'item_picture_review', 'item_picture_save', 'item_picture_delete', 'import_file_selection', 'import_read_file', 'import_assign_fields', 'import_items', 'print_preview', 'print_xlsx', 'print_ods', 'print_csv-ms', 'print_csv-oo', 'print_pdf', 'print_pdfl'))); $getinfUUID = admFuncVariableIsValid($_GET, 'uuid', 'uuid'); $getOptionID = admFuncVariableIsValid($_GET, 'option_id', 'int', array('defaultValue' => 0)); $getFieldName = admFuncVariableIsValid($_GET, 'field_name', 'string', array('defaultValue' => "", 'directOutput' => true)); @@ -59,6 +60,7 @@ }, $getItemUUIDs); } $getBorrowed = admFuncVariableIsValid($_GET, 'item_borrowed', 'bool', array('defaultValue' => false)); + $getNewPicture = admFuncVariableIsValid($_GET, 'new_picture', 'bool', array('defaultValue' => false)); // check if module is active if ($gSettingsManager->getInt('inventory_module_enabled') === 0) { @@ -69,6 +71,12 @@ throw new Exception('SYS_NO_RIGHTS'); } + // when saving folders, check whether the subfolder in adm_my_files exists with the corresponding rights + if ((int)$gSettingsManager->get('inventory_item_picture_storage') === 1) { + // Create folder for item pictures in adm_my_files if necessary + FileSystemUtils::createDirectoryIfNotExists(ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures'); + } + switch ($getMode) { case 'list': $headline = $gL10n->get('SYS_INVENTORY'); @@ -391,6 +399,78 @@ echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_DELETED'))); } break; + + case 'item_picture_show': + $itemModule = new ItemService($gDb, $getiniUUID); + $itemModule->showItemPicture($getNewPicture); + break; + + case 'item_picture_show_modal': + $msg = ' + + '; + + echo $msg; + break; + + break; + + case 'item_picture_choose': + $headline = $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CHOOSE'); + $gNavigation->addUrl(CURRENT_URL, $headline); + + $item = new InventoryItemPresenter('adm_item_picture_choose'); + $item->setHeadline($headline); + $item->createPictureChooseForm($getiniUUID); + $item->show(); + break; + + case 'item_picture_upload': + $itemModule = new ItemService($gDb, $getiniUUID); + $itemModule->uploadItemPicture(); + + echo json_encode(array( + 'status' => 'success', + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_review', 'item_uuid' => $getiniUUID)) + )); + break; + + case 'item_picture_review': + $headline = $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_REVIEW'); + $gNavigation->addUrl(CURRENT_URL, $headline); + + $item = new InventoryItemPresenter('adm_item_picture_review'); + $item->setHeadline($headline); + $item->createPictureReviewForm($getiniUUID); + $item->show(); + break; + + case 'item_picture_save': + $itemModule = new ItemService($gDb, $getiniUUID); + $itemModule->saveItemPicture(); + + // back to the home page + // if url stack is bigger then 2 then delete until the edit page is reached + while (count($gNavigation->getStack()) > 2) { + $gNavigation->deleteLastUrl(); + } + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_SAVED'), 'url' => $gNavigation->getUrl())); + break; + + case 'item_picture_delete': + // check the CSRF token of the form against the session token + SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); + + $itemModule = new ItemService($gDb, $getiniUUID); + $itemModule->deleteItemPicture(); + + echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_DELETED'), 'url' => $gNavigation->getUrl())); + break; #endregion #region import case 'import_file_selection': diff --git a/src/Changelog/Service/ChangelogService.php b/src/Changelog/Service/ChangelogService.php index 8a4d00b15d..007fcc7482 100644 --- a/src/Changelog/Service/ChangelogService.php +++ b/src/Changelog/Service/ChangelogService.php @@ -524,6 +524,7 @@ public static function getFieldTranslations(): array 'inf_sequence' => 'SYS_ORDER', 'ini_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), 'ini_status' => array('name' => 'SYS_INVENTORY_STATUS'), + 'ini_picture' => array('name' => 'SYS_INVENTORY_ITEM_PICTURE'), 'ind_value_bool' => array('name' => 'SYS_VALUE', 'type' => 'BOOL'), 'ind_value_date' => array('name' => 'SYS_VALUE', 'type' => 'DATE'), 'ind_value_mail' => array('name' => 'SYS_VALUE', 'type' => 'EMAIL'), diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index b5753b935c..68e8f251ac 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -4,6 +4,11 @@ use Admidio\Infrastructure\Exception; use Admidio\Infrastructure\Database; +use Admidio\Infrastructure\Image; +use Admidio\Infrastructure\Utils\PhpIniUtils; +use Admidio\Infrastructure\Utils\SystemInfoUtils; +use Admidio\Infrastructure\Utils\FileSystemUtils; +use Admidio\Inventory\Entity\Item; use Admidio\Inventory\ValueObjects\ItemsData; /** @@ -168,4 +173,191 @@ public function save(bool $multiEdit = false): void // Send notification to all users $this->itemRessource->sendNotification(); } + + /** + * Show the picture of the item. + * + * @throws Exception + */ + public function showItemPicture($getNewPicture = false) : void + { + global $gCurrentSession, $gSettingsManager; + $item = new Item($this->db, $this->itemRessource, $this->itemRessource->getItemId()); + + // Initialize default picture path + $picturePath = getThemedFile('/images/inventory-item-picture.png'); + $image = null; + + if ($item->getValue('ini_id') !== 0) { + if ($getNewPicture) { + // show temporary saved new picture from upload in database + if ($gSettingsManager->getInt('inventory_item_picture_storage') === 0) { + $image = new Image(); + $image->setImageFromData($gCurrentSession->getValue('ses_binary')); + } // show temporary saved new picture from upload in filesystem + else { + $picturePath = ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '_new.jpg'; + $image = new Image($picturePath); + } + } else { + // show picture from database + if ($gSettingsManager->getInt('inventory_item_picture_storage') === 0) { + if ((string)$item->getValue('ini_picture') !== '') { + $image = new Image(); + $image->setImageFromData($item->getValue('ini_picture')); + } else { + $image = new Image($picturePath); + } + } // show picture from folder adm_my_files + else { + $file = ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '.jpg'; + if (is_file($file)) { + $picturePath = $file; + } + $image = new Image($picturePath); + } + } + } else { + // If no item exists, show default picture + $image = new Image($picturePath); + } + + header('Content-Type: ' . $image->getMimeType()); + // Caching-Header setzen + header("Last-Modified: " . $item->getValue('ini_timestamp_changed', 'D, d M Y H:i:s') . " GMT"); + header("ETag: " . md5_file($picturePath)); + + $image->copyToBrowser(); + $image->delete(); + } + + /** + * Upload a new item picture. + * + * @throws Exception + */ + public function uploadItemPicture(): void + { + global $gCurrentSession, $gSettingsManager; + // Confirm cache picture + // check form field input and sanitized it from malicious content + $itemPictureUploadForm = $gCurrentSession->getFormObject($_POST['adm_csrf_token']); + $itemPictureUploadForm->validate($_POST); + + // File size + if ($_FILES['userfile']['error'][0] === UPLOAD_ERR_INI_SIZE) { + throw new Exception('SYS_PHOTO_FILE_TO_LARGE', array(round(PhpIniUtils::getUploadMaxSize() / 1024 ** 2))); + } + + // check if a file was really uploaded + if (!file_exists($_FILES['userfile']['tmp_name'][0]) || !is_uploaded_file($_FILES['userfile']['tmp_name'][0])) { + throw new Exception('SYS_NO_PICTURE_SELECTED'); + } + + // File ending + $imageProperties = getimagesize($_FILES['userfile']['tmp_name'][0]); + if ($imageProperties === false || !in_array($imageProperties['mime'], array('image/jpeg', 'image/png'), true)) { + throw new Exception('SYS_PHOTO_FORMAT_INVALID'); + } + + // Resolution control + $imageDimensions = $imageProperties[0] * $imageProperties[1]; + if ($imageDimensions > SystemInfoUtils::getProcessableImageSize()) { + throw new Exception('SYS_PHOTO_RESOLUTION_TO_LARGE', array(round(SystemInfoUtils::getProcessableImageSize() / 1000000, 2))); + } + + // Adjust picture to appropriate size + $itemImage = new Image($_FILES['userfile']['tmp_name'][0]); + $itemImage->setImageType('jpeg'); + $itemImage->scale(130, 170); + + if ($gSettingsManager->getInt('inventory_item_picture_storage') === 1) { + // Folder storage + $itemImage->copyToFile(null, ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '_new.jpg'); + } else { + // Database storage + $itemImage->copyToFile(null, $_FILES['userfile']['tmp_name'][0]); + $itemImageData = fread(fopen($_FILES['userfile']['tmp_name'][0], 'rb'), $_FILES['userfile']['size'][0]); + } + + $gCurrentSession->setValue('ses_binary', $itemImageData); + $gCurrentSession->save(); + + // delete image object + $itemImage->delete(); + } + + /** + * Save the picture of the item. + * + * @throws Exception + */ + public function saveItemPicture(): void + { + global $gLogger, $gSettingsManager, $gCurrentSession; + + if ($gSettingsManager->getInt('inventory_item_picture_storage') === 1) { + // Save picture in the file system + + // Check if a picture was saved for the user + $fileOld = ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '_new.jpg'; + if (is_file($fileOld)) { + $fileNew = ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '.jpg'; + try { + FileSystemUtils::deleteFileIfExists($fileNew); + + try { + FileSystemUtils::moveFile($fileOld, $fileNew); + } catch (\RuntimeException $exception) { + $gLogger->error('Could not move file!', array('from' => $fileOld, 'to' => $fileNew)); + // TODO + } + } catch (\RuntimeException $exception) { + $gLogger->error('Could not delete file!', array('filePath' => $fileNew)); + // TODO + } + } + } else { + // Save picture in the database + $item = new Item($this->db, $this->itemRessource, $this->itemRessource->getItemId()); + + // Check if a picture was saved for the user + if (strlen($gCurrentSession->getValue('ses_binary')) > 0) { + $this->db->startTransaction(); + // write the picture data into the database + $item->setValue('ini_picture', $gCurrentSession->getValue('ses_binary')); + $item->save(); + + // remove temporary picture data from session + $gCurrentSession->setValue('ses_binary', ''); + $gCurrentSession->save(); + $this->db->endTransaction(); + } + } + } + + /** + * Delete the picture of the item. + * + * @throws Exception + */ + public function deleteItemPicture(): void + { + global $gLogger, $gSettingsManager; + if ($gSettingsManager->getInt('inventory_item_picture_storage') === 1) { + // Folder storage, delete file + $filePath = ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '.jpg'; + try { + FileSystemUtils::deleteFileIfExists($filePath); + } catch (\RuntimeException $exception) { + $gLogger->error('Could not delete file!', array('filePath' => $filePath)); + // TODO + } + } else { + // Database storage, remove data from session + $item = new Item($this->db, $this->itemRessource, $this->itemRessource->getItemId()); + $item->setValue('ini_picture', ''); + $item->save(); + } + } } diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index d613e770fa..991884417e 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -7,6 +7,8 @@ use Admidio\Changelog\Service\ChangelogService; use Admidio\Infrastructure\Exception; use Admidio\Infrastructure\Utils\SecurityUtils; +use Admidio\Infrastructure\Utils\SystemInfoUtils; +use Admidio\Infrastructure\Utils\PhpIniUtils; use Admidio\Inventory\Entity\Item; use Admidio\Inventory\ValueObjects\ItemsData; use Admidio\UI\Presenter\FormPresenter; @@ -68,6 +70,15 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) } } + $this->addJavascript(' + function callbackItemPicture() { + var imgSrc = $("#adm_inventory_item_picture").attr("src"); + var timestamp = new Date().getTime(); + $("#adm_button_delete_picture").hide(); + $("#adm_inventory_item_picture").attr("src", imgSrc + "&" + timestamp); + } + '); + // show form $form = new FormPresenter( 'adm_item_edit_form', @@ -281,6 +292,15 @@ public function createEditForm(string $itemUUID = '', bool $getCopy = false) $this->assignSmartyVariable('lastUserEditedName', $item->getNameOfLastEditingUser()); $this->assignSmartyVariable('lastUserEditedTimestamp', $item->getValue('ini_timestamp_change')); + $this->assignSmartyVariable('urlItemPicture', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show', 'item_uuid' => $itemUUID))); + // the image can only be deleted if corresponding rights exist + if ($gCurrentUser->isAdministratorInventory() || in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { + $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); + if ($item->getValue('ini_picture') !== '' && $item->getValue('ini_picture') !== null) { + $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); + } + } + $form->addToHtmlPage(); $gCurrentSession->addFormObject($form); } @@ -915,4 +935,76 @@ function isSelect2Empty(selectId) { $form->addToHtmlPage(); $gCurrentSession->addFormObject($form); } + + /** + * Create the data for the picture upload form of a item. + * @param string $itemUUID UUID of the item that should be edited. + */ + public function createPictureChooseForm(string $itemUUID) + { + global $gCurrentSession, $gL10n; + // show form + $form = new FormPresenter( + 'adm_upload_picture_form', + 'modules/inventory.new-item-picture.upload.tpl', + SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_upload', 'item_uuid' => $itemUUID)), + $this, + array('enableFileUpload' => true) + ); + $form->addCustomContent( + 'item_picture_current', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT'), + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' + ); + $form->addFileUpload( + 'item_picture_upload_file', + $gL10n->get('SYS_SELECT_PHOTO'), + array( + 'property' => FormPresenter::FIELD_REQUIRED, + 'allowedMimeTypes' => array('image/jpeg', 'image/png'), + 'helpTextId' => array('SYS_INVENTORY_ITEM_PICTURE_RESTRICTIONS', array(round(SystemInfoUtils::getProcessableImageSize() / 1000000, 2), round(PhpIniUtils::getUploadMaxSize() / 1024 ** 2, 2))) + ) + ); + $form->addSubmitButton( + 'adm_button_upload', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_UPLOAD'), + array('icon' => 'bi-upload', 'class' => 'offset-sm-3') + ); + + $form->addToHtmlPage(); + $gCurrentSession->addFormObject($form); + } + + /** + * Create the data for the picture preview form of a item. + * @param string $itemUUID UUID of the item that should be edited. + */ + public function createPictureReviewForm(string $itemUUID) + { + global $gCurrentSession, $gL10n; + // show form + $form = new FormPresenter( + 'adm_review_picture_form', + 'modules/inventory.new-item-picture.tpl', + SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_save', 'item_uuid' => $itemUUID)), + $this + ); + $form->addCustomContent( + 'item_picture_current', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT'), + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' + ); + $form->addCustomContent( + 'item_picture_new', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_NEW'), + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_NEW') . '' + ); + $form->addSubmitButton( + 'adm_button_save', + $gL10n->get('SYS_APPLY'), + array('icon' => 'bi-upload', 'class' => 'offset-sm-3') + ); + $form->addToHtmlPage(); + $gCurrentSession->addFormObject($form); + } } diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 1f5d479c69..85dc48369a 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -793,6 +793,12 @@ public function prepareData(string $mode = 'html') : array } else { $headers[] = $gL10n->get('SYS_ABR_NO'); + if ($mode === 'html') { + // photo column + $headers[] = $gL10n->get('SYS_INVENTORY_ITEM_PICTURE'); + $columnAlign[] = 'center'; + } + } } @@ -865,6 +871,14 @@ public function prepareData(string $mode = 'html') : array $rowValues['data'][] = ''; } $rowValues['data'][] = $listRowNumber; + if ($mode === 'html') { + $itemPhotoUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show', 'item_uuid'=> $item['ini_uuid'])); + $itemPhotoModalUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show_modal', 'item_uuid'=> $item['ini_uuid'])); + $itemPhotoContent = ' + ' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . ' + '; + $rowValues['data'][] = $itemPhotoContent; + } } $content = $this->itemsData->getValue($infNameIntern, 'database'); diff --git a/src/UI/Presenter/PreferencesPresenter.php b/src/UI/Presenter/PreferencesPresenter.php index 2321e2ef72..e325c5859e 100644 --- a/src/UI/Presenter/PreferencesPresenter.php +++ b/src/UI/Presenter/PreferencesPresenter.php @@ -828,6 +828,14 @@ public function createInventoryForm(): string $gL10n->get('SYS_COMMON') ); + $selectBoxEntries = array('0' => $gL10n->get('SYS_DATABASE'), '1' => $gL10n->get('SYS_FOLDER')); + $formInventory->addSelectBox( + 'inventory_item_picture_storage', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURES_LOCATION'), + $selectBoxEntries, + array('defaultValue' => $formValues['inventory_item_picture_storage'], 'showContextDependentFirstEntry' => false, 'helpTextId' => 'SYS_INVENTORY_ITEM_PICTURES_LOCATION_DESC') + ); + $formInventory->addCheckbox( 'inventory_show_obsolete_select_field_options', $gL10n->get('SYS_SHOW_OBSOLETE_SELECT_FIELD_OPTIONS'), diff --git a/system/js/common_functions.js b/system/js/common_functions.js index 7a0cdfe9ad..d0cdd78eb0 100644 --- a/system/js/common_functions.js +++ b/system/js/common_functions.js @@ -127,6 +127,8 @@ function callUrlHideElement(elementId, url, csrfToken, callback) { $(entryDeleted).fadeOut("slow", callbackFutureRoles); } else if (callback === "callbackProfilePhoto") { callbackProfilePhoto(); + } else if (callback === "callbackItemPicture") { + callbackItemPicture(); } else { $(entryDeleted).fadeOut("slow"); } @@ -142,17 +144,21 @@ function callUrlHideElement(elementId, url, csrfToken, callback) { $(entryDeleted).fadeOut("slow", callbackFutureRoles); } else if (callback === 'callbackProfilePhoto') { callbackProfilePhoto(); + } else if (callback === "callbackItemPicture") { + callbackItemPicture(); } else { $(entryDeleted).fadeOut("slow"); } } - var tbodyElement = entryDeleted.closest("tbody"); - if (isTbodyEmpty(tbodyElement)) { - $(tbodyElement).fadeOut("slow"); - var tbodyElement2 = tbodyElement.previousElementSibling; - if (isTbodyEmpty(tbodyElement2)) { - $(tbodyElement2).fadeOut("slow"); + if (entryDeleted) { + var tbodyElement = entryDeleted.closest("tbody"); + if (isTbodyEmpty(tbodyElement)) { + $(tbodyElement).fadeOut("slow"); + var tbodyElement2 = tbodyElement.previousElementSibling; + if (isTbodyEmpty(tbodyElement2)) { + $(tbodyElement2).fadeOut("slow"); + } } } } else { diff --git a/themes/simple/css/admidio.css b/themes/simple/css/admidio.css index c6223bc17b..af447e5f0f 100644 --- a/themes/simple/css/admidio.css +++ b/themes/simple/css/admidio.css @@ -672,7 +672,7 @@ label { margin-left: 0.5rem; } -#adm_profile_photo { +#adm_profile_photo, #adm_inventory_item_picture { max-width: 300px; width: 100% !important; object-fit: cover !important; diff --git a/themes/simple/images/inventory-item-picture.png b/themes/simple/images/inventory-item-picture.png new file mode 100644 index 0000000000000000000000000000000000000000..e3eb804cc25ee290763448a800fbba070948fce3 GIT binary patch literal 167977 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 z zTr939cn(keQ^zn1M*=WNSU7YDx9*$8t+(8Yy|>(q*;{VK%uTmo_Lf_4>n*opX6^t@ ztpnO&Wi_lX#h13tw5t6}eraM}#`Yt&;+)+d!*xHqp{%K(ay2x1#f*qrj z0zHj6r-Yf=1K4xZEjV)R1b*$&55@04>uLCp|M!1!{_Fl6=e_Qgc>k+kfj@uVv+#sp z{zc%x9B#V#7A)~oon4d?2FD`poNe98iIMnr$hy_5am)UL_>T`=D3-wGFUR$Mv8t}^ z8Y`^jjNseVeviWMYq5jOi-n$#oG@)djNO#FZz07^Pzym-&XmsB4b-|Raob~HblL5q zGpaYyRX%zRhU^&VM1z<3kCd21iePaQMoEOVZ96pb1oBguI->m`O9&ISG~()@L# zo|V%Vl2eX`0B5I()?yVAYcooN*%?xlLf6B_pp>@CwA>h)V~exa>RZKczTnq~+*Nc> z&ibbu`%>i?E#)RdsZc2e9$zsHKupy+k_RCgquJhzDRhzixA&ck8|LP)c4AfWsq$&`(@#ASk2&F? zc=WM~;2nn$`H$4L9RsSAVYXRs^!?J_r)P67Kv;=AxA;{tihl^Gy7d6<~_% zO9obZnJt7FCnpTr)qtyZe_+X`er3keXp-^#3r&!5m{vjTfX$jY{ zJ9O2bxhY)yoe-pMq^=sAym!wd>44C+1v*lJCm^Cpmft&a2!+q{> z6h8juf55ZOI9csi{UTr=wv5N#>z=smZLh~OAN5fD?1mfZ*Do{5GojpfOMKn*laA_x ze>!*dYFzNCYl|Qw$3?aWqpwusY*vOt4f+s5QvY(xPI9C?<=Boh>!S+Ev`23igRv%b zr%$4Myi(LEq2KsVdjW}WJ~nPyv_QqNmw~2;{N&phm~D%X=6o9!$ARs{G$@_N4ie1n zOqo0|v_hdoW5nSGc-OJ`w~annekEH7@wX>hqLP{%^rh6|X1?8k>>c?+1$ zA2>*%Xi%5b6|o@7OG_F)ofrucZ^)yy$Z}FO9K4; zZl=37bN4)6G@KfxIfIv%&&x|iNu&*U8`BPHHg=3*wlXWE%$GryDKa#1_**qxqYfXIo?(J& z-z6de)=yvqCZTM~FXTew6FN0^H*OanW{H?98dWEOh75V5^*!%er~>aTnqn>KEwFAZvJBni8~n{T%liHwCqOIVMLx4-)3*uHIR zLqn{L6ke^8#0 zr6T+qQKB@ErX&>F(kvSX{X+|f4j-afu@neWX&Ox?ju28KbV5VL`lzmjgIxiadjvWp z*kKY0D<O6LnM%G`bw1Flyx$@B6l8=?v;zaRASFLzODE4KgEJVU* zzxEB>Ix~kgt5@l5DUP}vo0O6SCng31?78_?{J-CLHtv0oyTXV!f&nM4=F6)Ury-r)P>)_iQZIkxtaxKI}}yCI&cgXc_BvGlB!nad;`QA+GxUu7E+Xc51XBCaj_L32@IkVL0)UB<6#G5EI>S7j3#48XO zd5W6_lTcOwmjS255o82&lCiBoUxF4rOdA53H}_A)hYFd@Grc6xj>z--5hv14P=WPO zF`gR&cQVDaRF&54GZgN2epBL6>h%0u3W7}PNS3W(q6{d?VQSVpV z6m$bmJo@V&H4=*?zf2darDg6ynV-F;5V?MWFMa(xnCCan6EAQM>`?9?WuROM0GOL! zz;SoJBYxv)X9Pv~2y8IPd~q^#b)xi@{Or?D#QpDa7t9_w$SLp!l7(}oEB)H0SfE zziiuHxx(h;6LRO9&EYT*^}OFgq&}wqgf`&U3Rd41JyOR-?5yUGEvXCdD|&V?1@4=? zb&g0-CDqVBVvIT~+hSRCyK%)8IB{thv+<70SV+EJI>*M{Sp!lF+b8r|@;7Dmh;5;X z%|n`V-D|HKMtC33xO!(DpBGaxya=vXa{3;h$e8Lpl#S-yCbS&41x;I^fkhA=; zL}TE(8*aebHLHz)#H|5PGcMWy27>_(?3=}NPd^Fk*ROMt_Qz{rB!PLdVFRx)!;H17*Whc{T_3GGR11ZvS1ZOkN0Q3C;5xqaf2Noy zefnk2g3_Q|$e8=EhV+LV#vnpF+1eCpEf^Birtuy7f5i~}7&Bvl1}{TA6UTLu1{AgW z5v7|G^KC-MWbeTFa1yRd5;(bHBQ>XjFj=((;1gEUhVIl_i)jbEWNDQ=xC*j^Zj?m$ z!cr@yZ|SfIjn(bEOHp*m#`sPJDp)7;KChceUOr4FKO@AX{)#}yw-VHYw!4@=kA4L|P z95;d_HR&^gl_fsMttpsQhKph|BfnuKGRN-xs9*+FuN))DXwx|9_tysHZ5sreCo=KO za|j9&$g}$#H5+P8NP?=Pf?bDBUTe~>DP8E4Othn=J~y|~xDaLwue&3l{iQWx_uMuD z>PTF##HKMKbvClrl6S|uHHQ*cs^pm;h}zV=g;oSBJJGFmCcNe(+eoM~&QQ9IxoOd( z%Rc94-IYy;Y}bvl9P&<7L})`**U^4V++K;MjIz{8JVa!X(%TY2%u>eT{J{mxFDzhU zFrY8qNi0?;5lyYK!IXgcg$3Mk=MMbRJ?|#cwKGA;kLP(sDNk+SUU$DU?zU?O78e(& zE?j$oG^ON}0>_*I`DDQARNZAC3$fd)z+=dIzStxahKZ>$R!s-e-)z^Vmw7B#$&37Bo*ato^3oFSuww@%Cg`#J zRYAfA&ZZb+DVos+Fu8Ff?y+kJ78jNZ*VKYuef|_@A*7TrzjUZ}96%+lW3X^0tSBFs z1*~PqF80<|kmhr-FWe%}QZ08d^gd%zJx2mJU5A=uBU?7gGE!QO119i)*c-^k_Dw9E!nxC|ai<+I*$0{NJP=}8D{N@=tvx++Tt z`Z#IRc|b8tVsryVT9sRqQ$*~CI>~}oNVPQYD5G$y1PMapK60?NM3f~$%j8HHB^e&F zImkST(YaL~lP(zANsAYrB*$Eg1folikUP<%*LKU&N_HtF^JQ%V4Vb@`$o*8`>=;PZ@7uJELt5-^lP8f0+r z79(PdF^IfzC)+{;3W#nL5{LelI~j$>F=f7M(TYd%2RZ|eHEfBgOcZ9>O2raVL$85q zA`Oq@9ZQkgMCVmVp)nT`x#V{ljdyF4EL6Se81CaPOn^8`1UyfB| z{lnv-8+|-MGBvk$YqY7|PKtbjb5dvlWdRXxvyufdB-d`#SXogKP$3hu(;?~<~ zo$kzNy0oBGyK9y$2PmT zTP2o%OO>U?Z)MLMyYOizYuVBo<{_4Ob3xvU9FeTyLZ=c?zYa71Bq8`R3sOU?W!4iz zSL?=2V~>EGGY-xlp!aI1Fa(Z|!zWo?#){Da5)K_Ygk76ADXkN&4S!YB4!GRBl)pt- zc+_hBP?+2zr~D0BUsa_ewpM*DgsgKFTWoZD8@+PcA>1?x(!Q#cf@Q)L$Y{ix*>+C# zJz&H^82vEa7B-qdC{P)2O6WkblrtAWCL7%%lEWl24kA}z;}ks`fXmg?x|LI#SX}DD zD)x1&MeQnFMnb!e+GaZvnVh}D=q;PX4vp!;X9d~h7{2wied|`N*)WOarH&ER8OS61SJ-@jU#95mAyamWm;BL7;V#6k_`E+}Ss?AK(1R4Oq8sEh%P{K4D#& zkoX~#%qBj?yR@{7JMP@+4Kv&X9l<`VgbSCQQAi{uve9g5rgF6gAR*DO@!5$LGPZ-i z%v$QC77&(}-}Hl+&oEO!MCgxVB%nd^78?_XkSTG;udYC10XxD~P~D1**w))c?p&yY z%jkaevV+nyQn+0qF_tLmm=okngwiYYaK#LHEj+zSfsBp@m?av8?4!_ncbJK=A?kUj z_Dv;4rq?dzRf~|FDmK?Biww*{DOZWE{>Fj&vot$CSkczW+ES`4ScX>w>x{tcl8TE^ zh%s{hbv-*%+E;4m)kx;=0A!RI)qpLPM&Qvwy~!dy*~}DXE6cTO*Wh9Ix(D`N|5MB! zIDnb``!O>+i<$kin3>&==^6SrGdqjv*;!1_?#J}(EN15Buy1x2%X0_t(Btj{XpP8U zmLrqD_3uJXp-%N_jqm^X$GCCd4A!h#WlkZMDxFkn?2^qGCIERE#~jsIKr))QMitBw ziB=sYdmA`i@t1ZEe7I4|(A-PGXjXBC%c~aJvi)e)W0QycL+fhGAw*aO%rJBtYx~N( zj#+Z4KWkQ-f*oHv&umy#OR&<_q~^8-1U{tq2k0cGqw-fM znFM1v)HY^yN9Ke|juMYv^+bDs35;Hn{;6Y!@;K`3l6J_6{-n&m@m|3w+9aefNVG$> zGOd#~m)Ro&>Q6*zPPBT$@LRPKVkrb2*}no(YNaKf+^#+#mQ(L@3qgGImP`rKc|^mV}BVZ{_-#4&9C?aoc_2+dV>PaUH)lJgl7+>E%lO` zajX}9`g1tAu!J;7{7vq%gUwP)md$j*;V?wT`Za5C|9jm-X{8KZJ z7w7!rt2@&ty5;g4@MD&*iw=xYhF)b9fy^gqm%J-Lv|Ab!4%Jnuv_f>p!Y5g^V=SRH z)oS!hMlQ7@wZ_Gtx)#s;qgP?$wkiLnCqt|M{PyZ%)Ks4{gBBa!VR zF#=qdjA?HrkgUW9z#EQ^cz~X~n7+8Qgoi%+xADVU_hG}jwPcsiSIxsHkvl0g$t)}% z!qmh7*Piol*s*QONcXsArkxt(T0mw4n{QF%Lkd)5`NZeHj8lI1<=8&8nKxvlso~3` zYs~o18Q3>7i?bg7AiVW&{-3%i5d^dns)Do-1*_OHBIxGHsY?x02UE)?WIplF!b(m< zizE$W+!nAxvfGIU-|2h0bdFplRQ#-e%EQ8c~UZR5ibI%`RE%az@! zAvN`jUCOz^HYry8u~58P|29SSBd;)RZSYZsKF&fX0jX(lpzoEWxHI$j!7XwK%jc|> zQi^3KA#ifKyKPaH|s&%q7Sv)vz$x#Dn>@S&67LS-)$wNJ{t=ooD zoTPhjdbPK;I?-ZFz5w2H-o^Op4}OI88~EGu5-wke!(4^XqAMSvCj$u?2M!#>@pr$Q zTRezo;L1?m)Ry@iDqp57i&3BZFoulw4paCWC5urC;J{l<|K2GQ=aW9fobPl8_%HI96%#T z9$>B$MP1DhQ_5&rq88^Yfu6#=EB1P&V-ztKLmvszvyL3NsNkF-a#9?VYaqJnO-v*eE?!)`2nuTNYQ+jXsn=nuOz|D7|DNu4q;qT9qg=c?aGO)ts05` z(2kJ?LI^o13P%PF1uE2%nF$h7(_%Gq*os6sWDX-s=}c-~8|CBIGSSB1HEubuvGQ>) z6e5=gh?etFli->baY(#y0kWZGG--3MnfW)Nt{Fjy{K|t+HaQfwDUJjaPTI2Zk^UpTY{hgzHKi|oS$N2HEU;5{ zFB36XOs*N!2H!e$w@S=o`BO4Zr6`$$xBVwp!BTP3Xa{__ooEepac8V;h z2~r=Cl^A_4;#(o_iDUqFu{6n0Sh90=(*=oYQViq6#+8|_y4Hw7;^pKyk8-uje_{|Y zxq^6_N&=Etkn=Me9~P0MCRyL)B_+xr`(jtV#rADe7~0=y4-l$5*8rW&rl)7{xCb1EJ05*>VM_jOlYrDnx>J+zeWZPn z_`MF@bQCcGvF!vaR~IBtx3_rh7X8?tlBcVxFe0H52e-Wmt)OdhV%jPQLA6BC@EzdQ zNSGE7;qYsj7fOIiYdNuf#Ko<6x_rT$NLGN4 zF=J^#MsXE09oQ3+Nt%F$0hGsRH~ck7goJYS#Dm<)+D>iwgtJ{3^kM_g{+5XbZC;&G zpG!TiN@v*m(g|vBs~M*A!F=gD>s#n%7D~DsE+4O@(ojQcC>nkdE_P(GQumUE(8x)V z6Fsw_yRu4T$=Xb1;jncydoq_U`%;&&7GOJKKau?c3EbBJLN)eY`j>x?H(hWUb{x4K znID8m@^+&nk?=Jwe>#ig*vB9tFD>DDPkxdp5na~z5>78_k&+w5QzYo9KHo(wL&BEj zlq5`T3n*2KUC|8%u zu7J?9O=`g@s;HD9gBcz6BU~S{ol;3L(xqL{Oq)%Vn_?WjGe=Z=>!H(~OV}JG$Mll{ z|Ff!cn?TOLvbnJ{e`$(6VQHON#(!qGRSQGW8e5j1;uTg+a!gFO@1YejB@m6@?ABE@ z7o*Y%Rj@>O#b5su{_%qsV#m%M7+}Z|7U7hoHWCo`F+Vpd$!)d-%*-CZgYJ1ZecZgQ zUH2T=$q1-xCncuyU-D%pH*MPV^+Eo?gyqX|bP!?3ZVPNzeOlv|6v)HwP5ef!~TUqVQqls8= ziB}gqnsB0w#v+-hw)WD`)?5-@uq~T!)33QA(>xm| zzl{aHjtQ;ZDnZL3IVN*=z$_e;mUlWgcL2})y+6j^eCT{^J8CC=GaHE>?g%d*!<8Ah zR5N3k5)RDF;P;>N3`|T6>e8e}%?j7U%D7rIq?w;%oLP33HM0IO$8xn7Ck=<%Ub$9l zqe*WP3C;r#pB(97?qgBIw+-T`9NSC@7O@)OOLwL9V3yrUnFJv*eUUHV<%DB zlu&HPQILFCe$JE7IElrj&?Oj{JXA*}-)IEw-)CoIF>IiORPo%_pZTrZ3jj@DGD&ir zy3lZ#sv~tZ#}?&%C3{`wubBK*jA(k47`*@u;h-nUSwz0(<@-*@hMw5`$~I~263zZ1 z;SaC>2~PX%Kg9d5`7i7`>PV~t|2%CbM|4$D&LsK?+Y(kr=JBQyxOLB7obrI<@zfI^ z!xWZxYju4UF%n|Lf!c{{AR>wWs05d-1a9~Q9#9FIPkhY3YiUyD6p%V;Z9=Hd9V5=e zbazFVo(HXaVMGyxGWq#Qt3WN|yF3}%frR-jBfumP!4H5`ptF^2{(1#*_b ztpxff@k;d?bECkN(9pbZshl;^h%WAVJ5klA5uDyO;kNBynvk1H1u7q6vQ29fapE(> zO?I%Z{>-<2WVSZp*(`j-&e(XUREkBHmOgn`sC+V;`}3L8A}&j3`0W7=AJ`@NaN3`fhFY^_?E|Niv#ti3dVwJI!xeWkUfBwI5;tPKtpZe|(aP-a{ z$jh=|m_o60%>!ZOmb{p@G#p~x@-klik{33Aj7j5=_%lp`&J^WaZ4^qb2F&04m>`Lo-3GNx=_IQbY_aNmYGCp(>YUx_Lj@ihH2%|A?01xelHS7ag_ zjT>f{Q?Z$4`k2uX?rEQgtIAW7eIzWkewh@f+HLs9I-6 zH{7&_38bZhw!#DnF~U{OH7pT)`^FZ-egiu$E=1GXMVOP*m&{JWO-+v|($tMYGFdX+ ziJFJ}DUEaIE0)S$hSocSCv6rHJ1SD4y|eZcK5uZuXml_BGBZVwKXD{uv#6EeiV7{L zbPAUZm6&?ZM=!!@FM1h%a&QqlkJySl9P-2~;DAG-NpdL?xK!ukiR-9R^ z=`PD1bW@WkA}6pjS^13wMyl-zq!h9Gu5wMYa;cr96ZMvbqT#cvWI&(3NH+|&&@%2+ zU3QL6rI%`yhkJ^;4yi=)@h=IBbh%a2datGxfxwstOx;SW%#4|vR+!$9)~F4$s0+vT zeUa28bt7Wk{KY@N4bOkg-y?0_gsqdCFdPou@XHyWNJBxQ{m3~=+Fy?E3w z{Q~~)^PdIghQ4c{i*M-}K53vlmkwb-x^j^bzbP`Sl1w|<2^prh-4!ahX8i(@!04fO zv7q&@VRb5zu5-8bG+x-%+k6Qr%6Jq)6=2ps1l`t=mnbeAkG_%^@JZZ~*2~s}A|ZnA zM7KSll~5Lu>Ijjxl|LycsO>AFYf&PwI%W$xbGwM(1=y z>+0S_S3X@jVb2o%_NX;6sxRuBjp8wQj)8O``qCYTB1nv7I9esxH;Z{h0v*y>F2mGc zV^RWGoBzcp*)|Mk{S){Z6i?fUn`5F{MCG<6B#`hsWO8LMgDcZU>lfBw>}OOKAtWag z;fcB<7E?h-Cmya{Ws=e`P4B#`w~_drKyjZDCYQ!7A0)bBF)=w2Gl3+bjeu%IS!U39 zX?%A`pmhtIAp_mdDB0whD{Z#@4J>^$m7tQky@SfT|$1sl6mjn(xRttcra zEG#WydF~+I`RYH$-R^V;NUymZW4^jWirxaEUZjQ8mFIk$Mk6L2heo$m`otq9(v>+1 z_O6hjIdqsWi1!<6C_ls=Y4+HE6Wt;tHcp-nT>O!GBL%t;pI};!n}u*+k|J5!VcOBp zHtfJ@>?Q+cXKl(=GXn#TkYrSWH&S^qfn>?3MezlNsy#=NcD(BDOv za-de758(5+S-teH{~78 z5Yvp91vaJ{`%^33EL^p%0&rw8ERwm|%21(Fl39({!2ihzIk9#6prSit3 zc2Z&md*XNbY0%}pi#A%lQB;XB44I#g<0pS-GN^3EPGw^Iy7=njo_mNElmkA zfQc&DMzsEK|M?;O?fcHfu48u5qwYmjO|XXTQsXLvYHioqK}wiEw1oY)?!mvk?8P|i z^pl9uf(@uUY@my6^tB5;r7=(nN>_@k7&J-Rm0k19$oBT8@B{2VwF`~4vOtR*Xg2OJ ze)`O<2<$uD!Ybw~M=?-?Sm$zFS!t30l5et1|ET?rfz=Zt0NOE8Ycs35kqOaz zm4~TQC0yxy?TjEGj;ml=kQ0!E^+BNkWmf4KLX`yC35#5_D=G&)=#y;*<1{|=#joOz zUjG&xv19y$?3fia_$RFCqxDf^bG#SGYh3kp&N7=X(pvCx|#no z^!l(HJwmCgq}E5d1S|A7z?jbAj`V3?ErdpKvdi?{Wr^66WR?aA4mo-ts4Zfag5rRMj5^m!34@tEpELCgpbant}n3 z9oao<&YWmA*BQEK@;_t9o%22ZD`fb2WK7*6GeUX8hn=)!jIqjJ$nR89zC zS{Z`7yOZi$X(ewtBHJowY)3Z&M-LlWLsnYzQa|MEkeMMTd{+)aK*OH7ne67>T%_VR zj-wwvu=G~I`su1j2eN#ksZk?ErO0p!^!)@&i}u9KYv1xNeCB&U!qns@|IPJei&5(4 zZ{-P@iJ5L&&CDIZ^6Wvp|F8cT&;FH@qd1{qN4?3MtwXr@S2XL;Flu4gDVpFn`(nLnKz~j#nBTN%Vj=!u1fSZ>bfT$jbIh zaS1BhI`IYMmISO%vYlN{l7EDw8(-nhk6m9aS!+D@rGC|i4TSSCo4$1Akbx!gaz!Hb z>-vrqNQ`#nwoV<$sARHBC2^F+b%Z4~*nnE_Mp}`5;|D*)KfeDX*tVU%BtBu29{WfjUWp4D0oid&b18i?(tm)a1+qT!uD@KU+M)|f z#{e;lC8isBJ(uJvY=m2&XIv*=T8FA=gbs2*Fxa8wq+*8#_*+q>qaSiVHPW>!cSV#OV(eXtu4W`uiB@$j=SLJ4QQ0D8S!@JR zk9qX;lhN-EIK;SguM!z;bmb}2EBBa3rNpQN86t1=!Z*>oEEaT1aA(WTxG7f!CfkdO z;n6e(qYv$12V=I}kuXG#xfF&@h$3N`LMrXvs55F^fNvu(y+fvg?EDaPTmUv%Low#7 z?iIPXo>;rKTyh)-wKN->fGj&QE7yr!gp84>7TXaTPD8LF<#MGN*Hr=Ou%--1W|;yrNbTmKQiaM!yuVsC)bFx6%f5}lMD#i zgi+QF-^Ff9tXE6)BaRb|w=p(G!JMD6#@it^GPZGiFb3!`7h@j>y($$`((36|haePI zjHY=;hP@W%K>;eZ3^M=4#M{m9f|*s3dyO5R4@1`?4SQQ?yAio(zreK>(ssuS8ARNYC^yS%$OY zckLuXzFc^$TkhFYZ&)_@@Ik)$1LtG!frD7p_ymzorj8~D`iGhm1KfDy%{cMcU&aM* zd>xM1wzV_=7D`ysCx&N^n*gwf7heD~VmuSiMV*ppOl^amljyoKTr5_d=qtlAqy^$p+1$Q#^g-^t^AngxP}!ap%pO@qvGS4JIcy@?39LkB&6Qf|wD%p2h8wX$ZRs?N0q` zE9GruBy%T&X_+|mV&)c$*cNzKrLGZHKSe?tBGEF}gcXd1HE{Ma;D7cj2*)sEy9v{| zTw8!^POREWO=%zswJM`dP1_)Ta)aVC_r5dqiiAGxQQia$-d6}E;=?9pGPV$aD==fz5D*o-Lec^Pm!_EF}A zqR`Bd9X@9#LO|qVMdBI)SeQA0fBeJ$ja}Q1P*oR0Gw2?~F=Y;-so1tO8E5)W_$<(>)1Z9M zfxuUGP9HEI2q1?2t3qR-s(VK}yZ2CgAYvisZgAw*UyMaTV}X8!QmhAI%hI->C!nl_ zwFiPaqMCA;U|$-VF`Sc}+)4i>iA@wT=*xzx&l9;ZWhdiS3C7N)Ly&4$hGZ9PZL1Os zH0YWf7ZpKr1?)hE{t%P2`*fPIab=SX7Hgtu%UjV_+c+Wca+q8Jof09atmH#6QBdlsjm4;Vn&+{J9-W} z*@{50`jH*l4OhrR<4i6!g$K}{U(a+o}8sAkh5i7p>?eF1-H{OD^YgPkNBmQUYy3
PG}m%8RTzE9H;@Rz^!Ju19ezBD-4;4$-VSnw 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
{$l10n->get('SYS_PROPERTIES')}
- {foreach $elements as $key => $itemField} - {if {string_contains haystack=$key needle="INF-"} && $key != "INF-ITEMNAME"} - {if $itemField.type == 'checkbox'} - {include 'sys-template-parts/form.checkbox.tpl' data=$itemField} - {elseif $itemField.type == 'multiline'} - {include 'sys-template-parts/form.multiline.tpl' data=$itemField} - {elseif $itemField.type == 'radio'} - {include 'sys-template-parts/form.radio.tpl' data=$itemField} - {elseif $itemField.type == 'select'} - {include 'sys-template-parts/form.select.tpl' data=$itemField} - {else} - {if !{string_contains haystack=$key needle="_time"}} - {include 'sys-template-parts/form.input.tpl' data=$itemField} +
+
+ {foreach $elements as $key => $itemField} + {if {string_contains haystack=$key needle="INF-"} && $key != "INF-ITEMNAME"} + {if $itemField.type == 'checkbox'} + {include 'sys-template-parts/form.checkbox.tpl' data=$itemField} + {elseif $itemField.type == 'multiline'} + {include 'sys-template-parts/form.multiline.tpl' data=$itemField} + {elseif $itemField.type == 'radio'} + {include 'sys-template-parts/form.radio.tpl' data=$itemField} + {elseif $itemField.type == 'select'} + {include 'sys-template-parts/form.select.tpl' data=$itemField} + {else} + {if !{string_contains haystack=$key needle="_time"}} + {include 'sys-template-parts/form.input.tpl' data=$itemField} + {/if} + {/if} {/if} + {/foreach} +
+
+ {$l10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT')} + {if isset($urlItemPictureUpload)} + {/if} - {/if} - {/foreach} +
+
{if {array_key_exists array=$elements key='item_copy_number'}} diff --git a/themes/simple/templates/modules/inventory.new-item-picture.tpl b/themes/simple/templates/modules/inventory.new-item-picture.tpl new file mode 100644 index 0000000000..8b628f5dcd --- /dev/null +++ b/themes/simple/templates/modules/inventory.new-item-picture.tpl @@ -0,0 +1,9 @@ +
+ {include 'sys-template-parts/form.custom-content.tpl' data=$elements['item_picture_current']} + {include 'sys-template-parts/form.custom-content.tpl' data=$elements['item_picture_new']} + + + {include 'sys-template-parts/form.button.tpl' data=$elements['adm_button_save']} + diff --git a/themes/simple/templates/modules/inventory.new-item-picture.upload.tpl b/themes/simple/templates/modules/inventory.new-item-picture.upload.tpl new file mode 100644 index 0000000000..ddc876b734 --- /dev/null +++ b/themes/simple/templates/modules/inventory.new-item-picture.upload.tpl @@ -0,0 +1,11 @@ +
+
{$l10n->get('SYS_REQUIRED_INPUT')}
+ + {include 'sys-template-parts/form.input.tpl' data=$elements['adm_csrf_token']} + {include 'sys-template-parts/form.custom-content.tpl' data=$elements['item_picture_current']} + {include 'sys-template-parts/form.file.tpl' data=$elements['item_picture_upload_file']} + + {include 'sys-template-parts/form.button.tpl' data=$elements['adm_button_upload']} + diff --git a/themes/simple/templates/preferences/preferences.inventory.tpl b/themes/simple/templates/preferences/preferences.inventory.tpl index 02b6f97117..3f3bb59d95 100644 --- a/themes/simple/templates/preferences/preferences.inventory.tpl +++ b/themes/simple/templates/preferences/preferences.inventory.tpl @@ -44,6 +44,7 @@ {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_items_per_page']} {include 'sys-template-parts/form.input.tpl' data=$elements['inventory_field_history_days']} {include 'sys-template-parts/form.separator.tpl' data=$elements['inventory_separator_general_settings']} + {include 'sys-template-parts/form.select.tpl' data=$elements['inventory_item_picture_storage']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_show_obsolete_select_field_options']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_items_disable_borrowing']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements['inventory_system_field_names_editable']} From 0c74488014b3ea276bfaa2c243f7853f35da0469 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 01:45:35 +0200 Subject: [PATCH 18/25] fix: item pictures --- src/Inventory/Service/ItemService.php | 6 +++--- src/UI/Presenter/InventoryItemPresenter.php | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index 68e8f251ac..5bcd8c1eea 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -278,11 +278,11 @@ public function uploadItemPicture(): void // Database storage $itemImage->copyToFile(null, $_FILES['userfile']['tmp_name'][0]); $itemImageData = fread(fopen($_FILES['userfile']['tmp_name'][0], 'rb'), $_FILES['userfile']['size'][0]); + + $gCurrentSession->setValue('ses_binary', $itemImageData); + $gCurrentSession->save(); } - $gCurrentSession->setValue('ses_binary', $itemImageData); - $gCurrentSession->save(); - // delete image object $itemImage->delete(); } diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index 991884417e..f0da7819a6 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -296,7 +296,8 @@ function callbackItemPicture() { // the image can only be deleted if corresponding rights exist if ($gCurrentUser->isAdministratorInventory() || in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); - if ($item->getValue('ini_picture') !== '' && $item->getValue('ini_picture') !== null) { + if ((string)$item->getValue('ini_picture') !== '' && $gSettingsManager->getInt('inventory_item_picture_storage') === 0 + || is_file(ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $items->getItemId() . '.jpg') && $gSettingsManager->getInt('inventory_item_picture_storage') === 1) { $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); } } From 16a2911b17d8a56aefcbae4605f414e0b8a5acf8 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 02:11:55 +0200 Subject: [PATCH 19/25] fix: improve status log entries for item status changes --- src/Inventory/Entity/Item.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Inventory/Entity/Item.php b/src/Inventory/Entity/Item.php index af548d544a..e5247843b1 100644 --- a/src/Inventory/Entity/Item.php +++ b/src/Inventory/Entity/Item.php @@ -207,11 +207,15 @@ protected function adjustLogEntry(LogChanges $logEntry): void // If the item status is changed convert the status id to the actual status text if ($logEntry->getValue('log_field') === 'ini_status') { global $gDb; - $itemStatusId = $logEntry->getValue('log_value_new'); + $itemStatusIdNew = $logEntry->getValue('log_value_new'); + $itemStatusIdOld = $logEntry->getValue('log_value_old'); $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); - if ($option->readDataById($itemStatusId)) { + if ($option->readDataById($itemStatusIdNew)) { $logEntry->setValue('log_value_new', Language::translateIfTranslationStrId($option->getValue('ifo_value'))); } + if ($option->readDataById($itemStatusIdOld)) { + $logEntry->setValue('log_value_old', Language::translateIfTranslationStrId($option->getValue('ifo_value'))); + } } $logEntry->setValue('log_record_name', $itemName); From 5ab0964b72b5760733104a445d60f1d1f7db6fdb Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 10:27:01 +0200 Subject: [PATCH 20/25] fix: update inventory picture URLs to use the correct path --- modules/inventory.php | 4 ++-- src/Inventory/Entity/Item.php | 4 ++-- src/Inventory/Service/ItemService.php | 4 ++-- src/UI/Presenter/InventoryItemPresenter.php | 16 ++++++++-------- src/UI/Presenter/InventoryPresenter.php | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/inventory.php b/modules/inventory.php index efa340f5bc..3c025813cc 100644 --- a/modules/inventory.php +++ b/modules/inventory.php @@ -412,7 +412,7 @@ '; echo $msg; @@ -436,7 +436,7 @@ echo json_encode(array( 'status' => 'success', - 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_review', 'item_uuid' => $getiniUUID)) + 'url' => SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_review', 'item_uuid' => $getiniUUID)) )); break; diff --git a/src/Inventory/Entity/Item.php b/src/Inventory/Entity/Item.php index e5247843b1..e9565660a2 100644 --- a/src/Inventory/Entity/Item.php +++ b/src/Inventory/Entity/Item.php @@ -207,8 +207,8 @@ protected function adjustLogEntry(LogChanges $logEntry): void // If the item status is changed convert the status id to the actual status text if ($logEntry->getValue('log_field') === 'ini_status') { global $gDb; - $itemStatusIdNew = $logEntry->getValue('log_value_new'); - $itemStatusIdOld = $logEntry->getValue('log_value_old'); + $itemStatusIdNew = (int)$logEntry->getValue('log_value_new'); + $itemStatusIdOld = (int)$logEntry->getValue('log_value_old'); $option = new SelectOptions($gDb, $this->mItemsData->getProperty('STATUS', 'inf_id')); if ($option->readDataById($itemStatusIdNew)) { $logEntry->setValue('log_value_new', Language::translateIfTranslationStrId($option->getValue('ifo_value'))); diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index 5bcd8c1eea..45041ef01b 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -203,8 +203,8 @@ public function showItemPicture($getNewPicture = false) : void // show picture from database if ($gSettingsManager->getInt('inventory_item_picture_storage') === 0) { if ((string)$item->getValue('ini_picture') !== '') { - $image = new Image(); - $image->setImageFromData($item->getValue('ini_picture')); + $image = new Image(); + $image->setImageFromData($item->getValue('ini_picture')); } else { $image = new Image($picturePath); } diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index f0da7819a6..af41f3ad7d 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -292,13 +292,13 @@ function callbackItemPicture() { $this->assignSmartyVariable('lastUserEditedName', $item->getNameOfLastEditingUser()); $this->assignSmartyVariable('lastUserEditedTimestamp', $item->getValue('ini_timestamp_change')); - $this->assignSmartyVariable('urlItemPicture', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show', 'item_uuid' => $itemUUID))); + $this->assignSmartyVariable('urlItemPicture', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show', 'item_uuid' => $itemUUID))); // the image can only be deleted if corresponding rights exist if ($gCurrentUser->isAdministratorInventory() || in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { - $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); + $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); if ((string)$item->getValue('ini_picture') !== '' && $gSettingsManager->getInt('inventory_item_picture_storage') === 0 || is_file(ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $items->getItemId() . '.jpg') && $gSettingsManager->getInt('inventory_item_picture_storage') === 1) { - $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); + $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); } } @@ -948,14 +948,14 @@ public function createPictureChooseForm(string $itemUUID) $form = new FormPresenter( 'adm_upload_picture_form', 'modules/inventory.new-item-picture.upload.tpl', - SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_upload', 'item_uuid' => $itemUUID)), + SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_upload', 'item_uuid' => $itemUUID)), $this, array('enableFileUpload' => true) ); $form->addCustomContent( 'item_picture_current', $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT'), - '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' ); $form->addFileUpload( 'item_picture_upload_file', @@ -987,18 +987,18 @@ public function createPictureReviewForm(string $itemUUID) $form = new FormPresenter( 'adm_review_picture_form', 'modules/inventory.new-item-picture.tpl', - SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_save', 'item_uuid' => $itemUUID)), + SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_save', 'item_uuid' => $itemUUID)), $this ); $form->addCustomContent( 'item_picture_current', $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT'), - '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . '' ); $form->addCustomContent( 'item_picture_new', $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_NEW'), - '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_NEW') . '' + '' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_NEW') . '' ); $form->addSubmitButton( 'adm_button_save', diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 85dc48369a..8ee0efe79a 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -872,8 +872,8 @@ public function prepareData(string $mode = 'html') : array } $rowValues['data'][] = $listRowNumber; if ($mode === 'html') { - $itemPhotoUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show', 'item_uuid'=> $item['ini_uuid'])); - $itemPhotoModalUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory/inventory.php', array('mode' => 'item_picture_show_modal', 'item_uuid'=> $item['ini_uuid'])); + $itemPhotoUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show', 'item_uuid'=> $item['ini_uuid'])); + $itemPhotoModalUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show_modal', 'item_uuid'=> $item['ini_uuid'])); $itemPhotoContent = ' ' . $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_CURRENT') . ' '; From 7f783e5af365d7147876f99352655010d90097f5 Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 11:17:17 +0200 Subject: [PATCH 21/25] feat: replace HtmlTable with smarty templating for PDF export --- src/Inventory/Service/ExportService.php | 24 +++++++++---------- .../modules/inventory.list.export.tpl | 18 ++++++++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 themes/simple/templates/modules/inventory.list.export.tpl diff --git a/src/Inventory/Service/ExportService.php b/src/Inventory/Service/ExportService.php index 1f800176ff..0f35aa97ce 100644 --- a/src/Inventory/Service/ExportService.php +++ b/src/Inventory/Service/ExportService.php @@ -19,7 +19,6 @@ use Admidio\UI\Presenter\InventoryPresenter; // PHP namespaces -use HtmlTable; use InvalidArgumentException; /** * @brief Class with methods to display the module pages. @@ -104,20 +103,19 @@ public function createExport(string $mode = 'pdf'): void // add a page $pdf->AddPage(); - // Create table object for display - $exportTable = new HtmlTable('adm_inventory_table', $inventoryPage, false, false, 'table'); + // Using Smarty templating engine for exporting table in PDF + $smarty = $inventoryPage->createSmartyObject(); + $smarty->assign('attributes', array('border' => '1', 'cellpadding' => '1')); + $smarty->assign('column_align', $data['column_align']); + $smarty->assign('headers', $data['headers']); + $smarty->assign('headersStyle', 'font-size:10;font-weight:bold;background-color:#C7C7C7;'); + $smarty->assign('rows', $data['rows']); + $smarty->assign('rowsStyle', 'font-size:10;'); - $exportTable->addAttribute('border', '1'); - $exportTable->addAttribute('cellpadding', '1'); + // Fetch the HTML table from our Smarty template + $htmlTable = $smarty->fetch('modules/inventory.list.export.tpl'); - $exportTable->setColumnAlignByArray($data['column_align']); - $exportTable->addRowHeadingByArray($data['headers'],'', array('style' => 'font-size:10;font-weight:bold;background-color:#C7C7C7;')); - - foreach ($data['rows'] as $row) { - $exportTable->addRowByArray($row['data'], '', array('style' => 'font-size:10;')); - } - - $pdf->writeHTML($exportTable->getHtmlTable(), true, false, true); + $pdf->writeHTML($htmlTable, true, false, true); $file = ADMIDIO_PATH . FOLDER_DATA . '/temp/' . $filename; $pdf->Output($file, 'F'); header('Content-Type: application/pdf'); diff --git a/themes/simple/templates/modules/inventory.list.export.tpl b/themes/simple/templates/modules/inventory.list.export.tpl new file mode 100644 index 0000000000..795daea963 --- /dev/null +++ b/themes/simple/templates/modules/inventory.list.export.tpl @@ -0,0 +1,18 @@ +
+ + + {foreach $headers as $key => $header} + + {/foreach} + + + + {foreach $rows as $row} + + {foreach $row.data as $key => $cell} + + {/foreach} + + {/foreach} + +
{$header}
{$cell}
\ No newline at end of file From 1b1719ec75b14521b47407a4c345a19927f6808e Mon Sep 17 00:00:00 2001 From: Mathias Huth Date: Sat, 2 Aug 2025 13:52:53 +0200 Subject: [PATCH 22/25] feat: provide possibility to disable item pictures and enable seting for scaling --- install/db_scripts/preferences.php | 3 + languages/en.xml | 4 +- modules/inventory.php | 5 +- src/Inventory/Service/ItemService.php | 3 +- src/UI/Presenter/InventoryItemPresenter.php | 17 +++-- src/UI/Presenter/InventoryPresenter.php | 4 +- src/UI/Presenter/PreferencesPresenter.php | 21 ++++++ .../templates/modules/inventory.item.edit.tpl | 66 ++++++++++--------- .../preferences/preferences.inventory.tpl | 44 ++++++++++++- 9 files changed, 121 insertions(+), 46 deletions(-) diff --git a/install/db_scripts/preferences.php b/install/db_scripts/preferences.php index c13fe78c42..183a73e29c 100644 --- a/install/db_scripts/preferences.php +++ b/install/db_scripts/preferences.php @@ -150,7 +150,10 @@ 'inventory_module_enabled' => '2', 'inventory_items_per_page' => '25', 'inventory_field_history_days' => '365', + 'inventory_item_picture_enabled' => '1', 'inventory_item_picture_storage' => '0', + 'inventory_item_picture_width' => '130', + 'inventory_item_picture_height' => '170', 'inventory_show_obsolete_select_field_options' => '1', 'inventory_system_field_names_editable' => '0', 'inventory_allow_keeper_edit' => '0', diff --git a/languages/en.xml b/languages/en.xml index 299d052ee6..595c5c7373 100644 --- a/languages/en.xml +++ b/languages/en.xml @@ -820,6 +820,8 @@ Current item picture Delete item picture Item picture successfully deleted + Enable item pictures + If this option is enabled, item pictures can be uploaded and displayed. If disabled, no item pictures can be uploaded and existing item pictures will not be displayed. (Default: yes) New item picture The picture may have a maximum resolution of #VAR1# MegaPixels. The picture file must not be larger than #VAR1# MB and must be in JPG or PNG format. Review new item picture @@ -827,7 +829,7 @@ Upload item picture Do you want to delete the item picture? Storage location of Item pictures - Please define where to store item pictures (in the database or in folder adm_my_files). When a change is made, current pictures are not copied to new location. (default: database) + Please define where to store item pictures (in the database or in folder adm_my_files). When a change is made, current pictures are not copied to new location. If Database is selected, the uploaded picture is scaled to 130 x 170 pixels. (default: Database) Reinstate the item Do you really want to reinstate the item? Item successfully reinstated diff --git a/modules/inventory.php b/modules/inventory.php index 3c025813cc..3dda53b0a6 100644 --- a/modules/inventory.php +++ b/modules/inventory.php @@ -559,9 +559,8 @@ break; } } catch (Throwable $e) { - if (in_array($getMode, array('field_save', 'field_delete', 'sequence', 'item_save', 'import_read_file', 'import_items'))) { - echo// PHP namespaces - json_encode(array('status' => 'error', 'message' => $e->getMessage())); + if (in_array($getMode, array('field_save', 'field_delete', 'sequence', 'item_save', 'item_picture_upload', 'item_picture_save', 'item_picture_delete', 'import_read_file', 'import_items'))) { + echo json_encode(array('status' => 'error', 'message' => $e->getMessage())); } else { $gMessage->show($e->getMessage()); } diff --git a/src/Inventory/Service/ItemService.php b/src/Inventory/Service/ItemService.php index 45041ef01b..43e1141d2d 100644 --- a/src/Inventory/Service/ItemService.php +++ b/src/Inventory/Service/ItemService.php @@ -269,13 +269,14 @@ public function uploadItemPicture(): void // Adjust picture to appropriate size $itemImage = new Image($_FILES['userfile']['tmp_name'][0]); $itemImage->setImageType('jpeg'); - $itemImage->scale(130, 170); if ($gSettingsManager->getInt('inventory_item_picture_storage') === 1) { // Folder storage + $itemImage->scale($gSettingsManager->getInt('inventory_item_picture_width'), $gSettingsManager->getInt('inventory_item_picture_height')); $itemImage->copyToFile(null, ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $this->itemRessource->getItemId() . '_new.jpg'); } else { // Database storage + $itemImage->scale(130, 170); $itemImage->copyToFile(null, $_FILES['userfile']['tmp_name'][0]); $itemImageData = fread(fopen($_FILES['userfile']['tmp_name'][0], 'rb'), $_FILES['userfile']['size'][0]); diff --git a/src/UI/Presenter/InventoryItemPresenter.php b/src/UI/Presenter/InventoryItemPresenter.php index af41f3ad7d..1b9981ddea 100644 --- a/src/UI/Presenter/InventoryItemPresenter.php +++ b/src/UI/Presenter/InventoryItemPresenter.php @@ -292,13 +292,16 @@ function callbackItemPicture() { $this->assignSmartyVariable('lastUserEditedName', $item->getNameOfLastEditingUser()); $this->assignSmartyVariable('lastUserEditedTimestamp', $item->getValue('ini_timestamp_change')); - $this->assignSmartyVariable('urlItemPicture', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show', 'item_uuid' => $itemUUID))); - // the image can only be deleted if corresponding rights exist - if ($gCurrentUser->isAdministratorInventory() || in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { - $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); - if ((string)$item->getValue('ini_picture') !== '' && $gSettingsManager->getInt('inventory_item_picture_storage') === 0 - || is_file(ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $items->getItemId() . '.jpg') && $gSettingsManager->getInt('inventory_item_picture_storage') === 1) { - $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); + // only show the item picture if the module setting is enabled + if ($gSettingsManager->GetBool('inventory_item_picture_enabled')) { + $this->assignSmartyVariable('urlItemPicture', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show', 'item_uuid' => $itemUUID))); + // the image can only be deleted if corresponding rights exist + if ($gCurrentUser->isAdministratorInventory() || in_array($itemField->getValue('inf_name_intern'), $allowedFields)) { + $this->assignSmartyVariable('urlItemPictureUpload', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_choose', 'item_uuid' => $itemUUID))); + if ((string)$item->getValue('ini_picture') !== '' && $gSettingsManager->getInt('inventory_item_picture_storage') === 0 + || is_file(ADMIDIO_PATH . FOLDER_DATA . '/inventory_item_pictures/' . $items->getItemId() . '.jpg') && $gSettingsManager->getInt('inventory_item_picture_storage') === 1) { + $this->assignSmartyVariable('urlItemPictureDelete', 'callUrlHideElement(\'no_element\', \'' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_delete', 'item_uuid' => $itemUUID)) . '\', \'' . $gCurrentSession->getCsrfToken() . '\', \'callbackItemPicture\')'); + } } } diff --git a/src/UI/Presenter/InventoryPresenter.php b/src/UI/Presenter/InventoryPresenter.php index 8ee0efe79a..6491b163c8 100644 --- a/src/UI/Presenter/InventoryPresenter.php +++ b/src/UI/Presenter/InventoryPresenter.php @@ -793,7 +793,7 @@ public function prepareData(string $mode = 'html') : array } else { $headers[] = $gL10n->get('SYS_ABR_NO'); - if ($mode === 'html') { + if ($mode === 'html' && $gSettingsManager->GetBool('inventory_item_picture_enabled')) { // photo column $headers[] = $gL10n->get('SYS_INVENTORY_ITEM_PICTURE'); $columnAlign[] = 'center'; @@ -871,7 +871,7 @@ public function prepareData(string $mode = 'html') : array $rowValues['data'][] = ''; } $rowValues['data'][] = $listRowNumber; - if ($mode === 'html') { + if ($mode === 'html' && $gSettingsManager->GetBool('inventory_item_picture_enabled')) { $itemPhotoUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show', 'item_uuid'=> $item['ini_uuid'])); $itemPhotoModalUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/inventory.php', array('mode' => 'item_picture_show_modal', 'item_uuid'=> $item['ini_uuid'])); $itemPhotoContent = ' diff --git a/src/UI/Presenter/PreferencesPresenter.php b/src/UI/Presenter/PreferencesPresenter.php index 9f757f4f80..44542e43aa 100644 --- a/src/UI/Presenter/PreferencesPresenter.php +++ b/src/UI/Presenter/PreferencesPresenter.php @@ -828,6 +828,13 @@ public function createInventoryForm(): string $gL10n->get('SYS_COMMON') ); + $formInventory->addCheckbox( + 'inventory_item_picture_enabled', + $gL10n->get('SYS_INVENTORY_ITEM_PICTURE_ENABLED'), + (bool) $formValues['inventory_item_picture_enabled'], + array('helpTextId' => 'SYS_INVENTORY_ITEM_PICTURE_ENABLED_DESC') + ); + $selectBoxEntries = array('0' => $gL10n->get('SYS_DATABASE'), '1' => $gL10n->get('SYS_FOLDER')); $formInventory->addSelectBox( 'inventory_item_picture_storage', @@ -836,6 +843,20 @@ public function createInventoryForm(): string array('defaultValue' => $formValues['inventory_item_picture_storage'], 'showContextDependentFirstEntry' => false, 'helpTextId' => 'SYS_INVENTORY_ITEM_PICTURES_LOCATION_DESC') ); + $formInventory->addInput( + 'inventory_item_picture_width', + $gL10n->get('SYS_MAX_PHOTO_SIZE_WIDTH'), + $formValues['inventory_item_picture_width'], + array('type' => 'number', 'minNumber' => 1, 'maxNumber' => 9999, 'step' => 1) + ); + + $formInventory->addInput( + 'inventory_item_picture_height', + $gL10n->get('SYS_MAX_PHOTO_SIZE_HEIGHT'), + $formValues['inventory_item_picture_height'], + array('type' => 'number', 'minNumber' => 1, 'maxNumber' => 9999, 'step' => 1, 'helpTextId' => array('SYS_MAX_PHOTO_SIZE_DESC', array(130, 170))) + ); + $formInventory->addCheckbox( 'inventory_show_obsolete_select_field_options', $gL10n->get('SYS_SHOW_OBSOLETE_SELECT_FIELD_OPTIONS'), diff --git a/themes/simple/templates/modules/inventory.item.edit.tpl b/themes/simple/templates/modules/inventory.item.edit.tpl index 7df00f2e1a..bd3a6b231a 100644 --- a/themes/simple/templates/modules/inventory.item.edit.tpl +++ b/themes/simple/templates/modules/inventory.item.edit.tpl @@ -21,41 +21,45 @@
{$l10n->get('SYS_PROPERTIES')}
-
-
- {foreach $elements as $key => $itemField} - {if {string_contains haystack=$key needle="INF-"} && $key != "INF-ITEMNAME"} - {if $itemField.type == 'checkbox'} - {include 'sys-template-parts/form.checkbox.tpl' data=$itemField} - {elseif $itemField.type == 'multiline'} - {include 'sys-template-parts/form.multiline.tpl' data=$itemField} - {elseif $itemField.type == 'radio'} - {include 'sys-template-parts/form.radio.tpl' data=$itemField} - {elseif $itemField.type == 'select'} - {include 'sys-template-parts/form.select.tpl' data=$itemField} - {else} - {if !{string_contains haystack=$key needle="_time"}} - {include 'sys-template-parts/form.input.tpl' data=$itemField} + {if isset($urlItemPicture)} + -
+ {/if}
{if {array_key_exists array=$elements key='item_copy_number'}} diff --git a/themes/simple/templates/preferences/preferences.inventory.tpl b/themes/simple/templates/preferences/preferences.inventory.tpl index 3f3bb59d95..716445298b 100644 --- a/themes/simple/templates/preferences/preferences.inventory.tpl +++ b/themes/simple/templates/preferences/preferences.inventory.tpl @@ -1,6 +1,33 @@