diff --git a/config/vufind/Folio.ini b/config/vufind/Folio.ini index 6ab52375900..65870d59cc0 100644 --- a/config/vufind/Folio.ini +++ b/config/vufind/Folio.ini @@ -186,6 +186,12 @@ in_transit[] = "Open - Awaiting delivery" ; retrieved from FOLIO will be retained. ;vufind_sort = "enumchron" +; If set to true and there are no items attached to a FOLIO holdings record, +; VuFind will display the holdings summary, supplement, and indexes fields on the record +; holdings tab. +; Note: `hide_holdings[]` in config.ini can be used to suppress display of specific locations. +; show_holdings_no_items = true + [CourseReserves] ; If set to true, the course number will be prefixed on the course name; if false, ; only the name will be displayed: diff --git a/languages/HoldingStatus/en.ini b/languages/HoldingStatus/en.ini index 72871550e21..4ff81a5de49 100644 --- a/languages/HoldingStatus/en.ini +++ b/languages/HoldingStatus/en.ini @@ -1,6 +1,7 @@ availability_uncertain = "Uncertain" copies_ordered_on_date = "%%copies%% copies ordered on %%date%%" copy_ordered_on_date = "1 copy ordered on %%date%%" +holding_no_items_availability_message = "See full record" service_available_presentation = "In Library Use Only" service_loan = "Loan" service_presentation = "In Library Use" diff --git a/module/VuFind/src/VuFind/ILS/Driver/Folio.php b/module/VuFind/src/VuFind/ILS/Driver/Folio.php index 23a4c359a5b..03f74703c2c 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Folio.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Folio.php @@ -35,6 +35,7 @@ use Laminas\Http\Response; use VuFind\Exception\ILS as ILSException; use VuFind\I18n\Translator\TranslatorAwareInterface; +use VuFind\ILS\Logic\AvailabilityStatus; use VuFindHttp\HttpServiceAwareInterface as HttpServiceAwareInterface; use function array_key_exists; @@ -691,6 +692,38 @@ protected function getHoldingDetailsForItem($holding): array ); } + /** + * Support method for getHolding() -- return an array of item-level details from + * both FOLIO holdings and item records. + * + * Depending on where this method is called, $locationId will be the holdings record + * location (in the case where no items are attached to a holding) or the item record + * location (in cases where there are attached items). + * + * @param string $locationId Location identifier from FOLIO + * @param array $holdingDetails Holding details produced by getHoldingDetailsForItem() + * + * @return array + */ + protected function getItemFieldsFromLocAndHolding( + string $locationId, + array $holdingDetails, + ): array { + $locationData = $this->getLocationData($locationId); + $locationName = $locationData['name']; + return [ + 'is_holdable' => $this->isHoldable($locationName), + 'holdings_notes' => $holdingDetails['hasHoldingNotes'] + ? $holdingDetails['holdingNotes'] : null, + 'summary' => array_unique($holdingDetails['holdingsStatements']), + 'supplements' => $holdingDetails['holdingsSupplements'], + 'indexes' => $holdingDetails['holdingsIndexes'], + 'location' => $locationName, + 'location_code' => $locationData['code'], + 'folio_location_is_active' => $locationData['isActive'], + ]; + } + /** * Support method for getHolding() -- given a few key details, format an item * for inclusion in the return value. @@ -718,10 +751,7 @@ protected function formatHoldingItem( array_map([$this, 'formatNote'], $item->notes ?? []) ); $locationId = $item->effectiveLocation->id; - $locationData = $this->getLocationData($locationId); - $locationName = $locationData['name']; - $locationCode = $locationData['code']; - $locationIsActive = $locationData['isActive']; + // concatenate enumeration fields if present $enum = implode( ' ', @@ -741,8 +771,9 @@ protected function formatHoldingItem( $item->effectiveCallNumberComponents->callNumber ?? $item->itemLevelCallNumber ?? '' ); + $locAndHoldings = $this->getItemFieldsFromLocAndHolding($locationId, $holdingDetails); - return $callNumberData + [ + return $callNumberData + $locAndHoldings + [ 'id' => $bibId, 'item_id' => $item->id, 'holdings_id' => $holdingDetails['id'], @@ -752,16 +783,7 @@ protected function formatHoldingItem( 'status' => $item->status->name, 'duedate' => $dueDateValue, 'availability' => $item->status->name == 'Available', - 'is_holdable' => $this->isHoldable($locationName), - 'holdings_notes' => $holdingDetails['hasHoldingNotes'] - ? $holdingDetails['holdingNotes'] : null, 'item_notes' => !empty(implode($itemNotes)) ? $itemNotes : null, - 'summary' => array_unique($holdingDetails['holdingsStatements']), - 'supplements' => $holdingDetails['holdingsSupplements'], - 'indexes' => $holdingDetails['holdingsIndexes'], - 'location' => $locationName, - 'location_code' => $locationCode, - 'folio_location_is_active' => $locationIsActive, 'reserve' => 'TODO', 'addLink' => true, 'bound_with_records' => $boundWithRecords, @@ -834,6 +856,7 @@ public function getHolding($bibId, array $patron = null, array $options = []) $showDueDate = $this->config['Availability']['showDueDate'] ?? true; $showTime = $this->config['Availability']['showTime'] ?? false; $maxNumDueDateItems = $this->config['Availability']['maxNumberItems'] ?? 5; + $showHoldingsNoItems = $this->config['Holdings']['show_holdings_no_items'] ?? false; $dueDateItemCount = 0; $instance = $this->getInstanceByBibId($bibId); @@ -896,6 +919,25 @@ public function getHolding($bibId, array $patron = null, array $options = []) } $nextBatch[] = $nextItem; } + + // If there are no item records on this holding, we're going to create a fake one, + // fill it with data from the FOLIO holdings record, and make it not appear in + // the full record display using a non-visible AvailabilityStatus. + if ($number == 0 && $showHoldingsNoItems) { + $locAndHoldings = $this->getItemFieldsFromLocAndHolding($holding->effectiveLocationId, $holdingDetails); + $invisibleAvailabilityStatus = new AvailabilityStatus( + true, + 'HoldingStatus::holding_no_items_availability_message' + ); + $invisibleAvailabilityStatus->setVisibilityInHoldings(false); + $nextBatch[] = $locAndHoldings + [ + 'id' => $bibId, + 'callnumber' => $holdingDetails['holdingCallNumber'], + 'callnumber_prefix' => $holdingDetails['holdingCallNumberPrefix'], + 'reserve' => 'N', + 'availability' => $invisibleAvailabilityStatus, + ]; + } $items = array_merge( $items, $sortNeeded diff --git a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php index 34067c693c5..83c9ba344b1 100644 --- a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php +++ b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatus.php @@ -49,6 +49,13 @@ class AvailabilityStatus implements AvailabilityStatusInterface */ protected int $availability; + /** + * Item visibility in holdings tab + * + * @var bool + */ + protected bool $visibilityInHoldingsTab = true; + /** * Constructor * @@ -93,8 +100,20 @@ public function is(int $availability): bool */ public function isVisibleInHoldings(): bool { - // Can be overridden if the status should not be visible in the holdings tab, - return true; + return $this->visibilityInHoldingsTab; + } + + /** + * Set visibility in holdings tab. + * + * @param bool $visibilityInHoldingsTab Visibility toggle + * + * @return AvailabilityStatus + */ + public function setVisibilityInHoldings(bool $visibilityInHoldingsTab): AvailabilityStatus + { + $this->visibilityInHoldingsTab = $visibilityInHoldingsTab; + return $this; } /** diff --git a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatusInterface.php b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatusInterface.php index 78491613325..eeab3335704 100644 --- a/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatusInterface.php +++ b/module/VuFind/src/VuFind/ILS/Logic/AvailabilityStatusInterface.php @@ -93,6 +93,15 @@ public function is(int $availability): bool; */ public function isVisibleInHoldings(): bool; + /** + * Set visibility status. + * + * @param bool $visibilityInHoldingsTab Visibility toggle + * + * @return AvailabilityStatus + */ + public function setVisibilityInHoldings(bool $visibilityInHoldingsTab): AvailabilityStatus; + /** * Get status description. *