diff --git a/module/VuFind/src/VuFind/AjaxHandler/GetItemStatuses.php b/module/VuFind/src/VuFind/AjaxHandler/GetItemStatuses.php index 980c8822ee2..c092599912b 100644 --- a/module/VuFind/src/VuFind/AjaxHandler/GetItemStatuses.php +++ b/module/VuFind/src/VuFind/AjaxHandler/GetItemStatuses.php @@ -44,9 +44,12 @@ use VuFind\ILS\Logic\Holds; use VuFind\Session\Settings as SessionSettings; +use function array_map; +use function array_unique; use function count; use function in_array; use function is_array; +use function is_string; /** * "Get Item Status" AJAX handler @@ -127,7 +130,8 @@ protected function translateList($transPrefix, $list) { $transList = []; foreach ($list as $current) { - $transList[] = $this->translateWithPrefix($transPrefix, $current); + // $current can be an array if pickValue() is called with callnumbers + $transList[] = is_string($current) ? $this->translateWithPrefix($transPrefix, $current) : $current; } return $transList; } @@ -139,37 +143,27 @@ protected function translateList($transPrefix, $list) * @param array $rawList Array of values to choose from. * @param string $mode config.ini setting -- first, all or msg * @param string $msg Message to display if $mode == "msg" - * @param string $transPrefix Translator prefix to apply to values (false to - * omit translation of values) + * @param string $transPrefix Translator prefix to apply to values (false to omit translation of values) * - * @return string + * @return array */ protected function pickValue($rawList, $mode, $msg, $transPrefix = false) { // Make sure array contains only unique values: - $list = array_unique($rawList); - - // If there is only one value in the list, or if we're in "first" mode, - // send back the first list value: - if ($mode == 'first' || count($list) == 1) { - if ($transPrefix) { - return $this->translateWithPrefix($transPrefix, $list[0]); - } - return $list[0]; - } elseif (count($list) == 0) { - // Empty list? Return a blank string: - return ''; - } elseif ($mode == 'all') { - // All values mode? Return comma-separated values: - return implode( - ",\t", - $transPrefix ? $this->translateList($transPrefix, $list) : $list - ); - } else { + // array unique for multidimensional arrays due to callnumber array, + // can be slow for larger/more complex arrays + $list = array_map('unserialize', array_unique(array_map('serialize', $rawList))); + + // If we're in "first" mode, reduce list to first list value: + if ($mode == 'first' && count($list) > 0) { + $list = [$list[0]]; + } elseif ($mode == 'msg' && count($list) > 1) { // Message mode? Return the specified message, translated to the // appropriate language. - return $this->translate($msg); + return [$this->translate($msg)]; } + + return $transPrefix ? $this->translateList($transPrefix, $list) : $list; } /** @@ -219,17 +213,47 @@ protected function reduceServices(array $rawServices) } /** - * Create a delimited version of the call number to allow the Javascript code - * to handle the prefix appropriately. + * Create an array with the callnumber and prefix of the given item. + * + * @param array $item Item's holding data. + * + * @return array Associative array with the keys 'prefix' and 'callnumber' + */ + protected function getCallNumberArray(array $item): array + { + return [ + 'prefix' => $item['callnumber_prefix'] ?? '', + 'callnumber' => $item['callnumber'], + ]; + } + + /** + * Render the callnumber HTML. * - * @param string $prefix Callnumber prefix or empty string. - * @param string $callnumber Main call number. + * @param string $callnumberSetting The callnumber mode setting + * @param array $callnumbers Callnumbers to render * * @return string */ - protected function formatCallNo($prefix, $callnumber) + protected function renderCallnumbers(string $callnumberSetting, array $callnumbers): string { - return !empty($prefix) ? $prefix . '::::' . $callnumber : $callnumber; + $html = []; + + $callnumberHandler = $this->getCallnumberHandler($callnumbers, $callnumberSetting); + foreach ($callnumbers as $number) { + $displayCallnumber = $actualCallnumber = $number['callnumber']; + + if (!empty($number['prefix'])) { + $displayCallnumber = $number['prefix'] . ' ' . $displayCallnumber; + } + + $html[] = $this->renderer->render( + 'ajax/itemCallnumber', + compact('actualCallnumber', 'displayCallnumber', 'callnumberHandler') + ); + } + + return implode(",\t", $html); } /** @@ -255,10 +279,7 @@ protected function getItemStatus( $services = []; foreach ($record as $info) { // Store call number/location info: - $callNumbers[] = $this->formatCallNo( - $info['callnumber_prefix'] ?? '', - $info['callnumber'] - ); + $callNumbers[] = $this->getCallNumberArray($info); $locations[] = $info['location']; // Store all available services @@ -267,11 +288,6 @@ protected function getItemStatus( } } - $callnumberHandler = $this->getCallnumberHandler( - $callNumbers, - $callnumberSetting - ); - // Determine call number string based on findings: $callNumber = $this->pickValue( $callNumbers, @@ -304,13 +320,12 @@ protected function getItemStatus( 'id' => $record[0]['id'], 'availability' => $combinedAvailability->availabilityAsString(), 'availability_message' => $availabilityMessage, - 'location' => htmlentities($location, ENT_COMPAT, 'UTF-8'), + 'location' => htmlentities(implode(",\t", $location), ENT_COMPAT, 'UTF-8'), 'locationList' => false, 'reserve' => $reserve ? 'true' : 'false', 'reserve_message' => $this->translate($reserve ? 'on_reserve' : 'Not On Reserve'), - 'callnumber' => htmlentities($callNumber, ENT_COMPAT, 'UTF-8'), - 'callnumber_handler' => $callnumberHandler, + 'callnumberHtml' => $this->renderCallnumbers($callnumberSetting, $callNumber), ]; } @@ -330,49 +345,33 @@ protected function getItemStatusGroup($record, $callnumberSetting) // Summarize call number, location and availability info across all items: $locations = []; foreach ($record as $info) { - $availabilityStatus = $info['availability']; - // Find an available copy - if ($availabilityStatus->isAvailable()) { - if ('true' !== ($locations[$info['location']]['available'] ?? null)) { - $locations[$info['location']]['available'] = $availabilityStatus->getStatusDescription(); - } - } - // Check for a use_unknown_message flag - if ($availabilityStatus->is(AvailabilityStatusInterface::STATUS_UNKNOWN)) { - $locations[$info['location']]['status_unknown'] = true; - } // Store call number/location info: - $locations[$info['location']]['callnumbers'][] = $this->formatCallNo( - $info['callnumber_prefix'] ?? '', - $info['callnumber'] - ); + $locations[$info['location']]['callnumbers'][] = $this->getCallNumberArray($info); + $locations[$info['location']]['items'][] = $info; } // Build list split out by location: $locationList = []; foreach ($locations as $location => $details) { - $locationCallnumbers = array_unique($details['callnumbers']); // Determine call number string based on findings: - $callnumberHandler = $this->getCallnumberHandler( - $locationCallnumbers, - $callnumberSetting - ); $locationCallnumbers = $this->pickValue( - $locationCallnumbers, + $details['callnumbers'], $callnumberSetting, 'Multiple Call Numbers' ); + + // Get combined availability for location + $locationStatus = $this->availabilityStatusManager->combine($details['items']); + $locationInfo = [ - 'availability' => $details['available'] ?? false, + 'availability' => $locationStatus['availability'], 'location' => htmlentities( $this->translateWithPrefix('location_', $location), ENT_COMPAT, 'UTF-8' ), - 'callnumbers' => - htmlentities($locationCallnumbers, ENT_COMPAT, 'UTF-8'), - 'status_unknown' => $details['status_unknown'] ?? false, - 'callnumber_handler' => $callnumberHandler, + 'callnumberHtml' => + $this->renderCallnumbers($callnumberSetting, $locationCallnumbers), ]; $locationList[] = $locationInfo; } @@ -389,11 +388,11 @@ protected function getItemStatusGroup($record, $callnumberSetting) 'availability' => $combinedAvailability->availabilityAsString(), 'availability_message' => $this->getAvailabilityMessage($combinedAvailability), 'location' => false, - 'locationList' => $locationList, + 'locationList' => $this->renderer->render('ajax/itemLocationList', ['locationList' => $locationList]), 'reserve' => $reserve ? 'true' : 'false', 'reserve_message' => $this->translate($reserve ? 'on_reserve' : 'Not On Reserve'), - 'callnumber' => false, + 'callnumberHtml' => false, ]; } diff --git a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php index 83c9ba344b1..ba0f3418949 100644 --- a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php +++ b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php @@ -190,9 +190,9 @@ public function availabilityAsString(): string { switch ($this->availability) { case AvailabilityStatusInterface::STATUS_UNAVAILABLE: - return 'false'; + return 'unavailable'; case AvailabilityStatusInterface::STATUS_AVAILABLE: - return 'true'; + return 'available'; case AvailabilityStatusInterface::STATUS_UNKNOWN: return 'unknown'; default: diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AvailabilityStatus.php b/module/VuFind/src/VuFind/View/Helper/Root/AvailabilityStatus.php index dcd147d216b..f7892d88c4b 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/AvailabilityStatus.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/AvailabilityStatus.php @@ -70,6 +70,34 @@ class AvailabilityStatus extends \Laminas\View\Helper\AbstractHelper */ protected string $classUnknown = 'text-muted'; + /** + * Icon for available items. + * + * @var string + */ + protected string $iconAvailable = 'status-available'; + + /** + * Icon for unavailable items. + * + * @var string + */ + protected string $iconUnavailable = 'status-unavailable'; + + /** + * Icon for items where status is uncertain. + * + * @var string + */ + protected string $iconUncertain = 'status-uncertain'; + + /** + * Icon for items where status is unknown. + * + * @var string + */ + protected string $iconUnknown = 'status-unknown'; + /** * Message cache * @@ -98,6 +126,27 @@ public function getClass(AvailabilityStatusInterface $availabilityStatus): strin return $this->classUncertain; } + /** + * Get icon name for availability status. + * + * @param AvailabilityStatusInterface $availabilityStatus Availability Status + * + * @return string + */ + public function getIcon(AvailabilityStatusInterface $availabilityStatus): string + { + if ($availabilityStatus->is(\VuFind\ILS\Logic\AvailabilityStatusInterface::STATUS_UNAVAILABLE)) { + return $this->iconUnavailable; + } + if ($availabilityStatus->is(\VuFind\ILS\Logic\AvailabilityStatusInterface::STATUS_AVAILABLE)) { + return $this->iconAvailable; + } + if ($availabilityStatus->is(\VuFind\ILS\Logic\AvailabilityStatusInterface::STATUS_UNKNOWN)) { + return $this->iconUnknown; + } + return $this->iconUncertain; + } + /** * Render ajax status. * diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/HoldingsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/HoldingsTest.php index 6a48e8e428d..723f62560da 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/HoldingsTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/HoldingsTest.php @@ -184,7 +184,10 @@ public function testItemStatus( ); $this->assertEquals( 'Main Library', - $this->findCssAndGetText($page, '.result-body .callnumAndLocation .groupLocation .text-success') + $this->findCssAndGetText( + $page, + '.result-body .callnumAndLocation .groupLocation .text-' . $expectedType + ) ); } } else { @@ -196,8 +199,8 @@ public function testItemStatus( } else { // No extra items to care for: if ('group' === $multipleLocations) { - // Unknown status displays as warning: - $type = null === $availability ? 'warning' : 'danger'; + // Unknown status displays as muted: + $type = null === $availability ? 'muted' : 'danger'; $selector = ".result-body .callnumAndLocation .groupLocation .text-$type"; } else { $selector = '.result-body .callnumAndLocation .location'; diff --git a/themes/bootstrap3/js/check_item_statuses.js b/themes/bootstrap3/js/check_item_statuses.js index d6acf427ef9..7d7fd509947 100644 --- a/themes/bootstrap3/js/check_item_statuses.js +++ b/themes/bootstrap3/js/check_item_statuses.js @@ -4,26 +4,6 @@ VuFind.register('itemStatuses', function ItemStatuses() { var _checkItemHandlers = {}; var _handlerUrls = {}; - function formatCallnumbers(callnumber, callnumber_handler) { - var cns = callnumber.split(',\t'); - for (var i = 0; i < cns.length; i++) { - // If the call number has a special delimiter, it indicates a prefix that - // should be used for display but not for sorting/searching. - var actualCallNumber = cns[i]; - var displayCallNumber = cns[i]; - var parts = cns[i].split('::::'); - if (parts.length > 1) { - displayCallNumber = parts[0] + " " + parts[1]; - actualCallNumber = parts[1]; - } - - cns[i] = callnumber_handler - ? '' + displayCallNumber + '' - : displayCallNumber; - } - return cns.join(',\t'); - } - function displayItemStatus(result, el) { el.querySelectorAll('.status').forEach((status) => { status.innerHTML = result.availability_message; @@ -57,45 +37,18 @@ VuFind.register('itemStatuses', function ItemStatuses() { // No data is available -- hide the entire status area: el.querySelectorAll('.callnumAndLocation,.status').forEach((e) => e.classList.add('hidden')); } else if (result.locationList) { - // We have multiple locations -- build appropriate HTML and hide unwanted labels: + // We have multiple locations - hide unwanted labels and display HTML from response: el.querySelectorAll('.callnumber,.hideIfDetailed,.location').forEach((e) => e.classList.add('hidden')); - var locationListHTML = ""; - for (var x = 0; x < result.locationList.length; x++) { - locationListHTML += '