From 6db2c11e04bb09054f589ab0f45aeebecb4ab4a8 Mon Sep 17 00:00:00 2001 From: Kodi Date: Thu, 22 Aug 2024 09:11:02 -0700 Subject: [PATCH 01/56] BiblioCommons Import Fix issue where importing BiblioCommons data for Sierra libraries is not working correctly Update release notes Tested on my local by running a BiblioCommons import with shelves and staff list csv files --- code/web/Drivers/Sierra.php | 4 +++- code/web/migrations/importBiblioCommonsData.php | 17 ++++++++++++++++- code/web/release_notes/24.08.01.MD | 6 +++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/code/web/Drivers/Sierra.php b/code/web/Drivers/Sierra.php index 2b783cd98a..d5896c1c22 100644 --- a/code/web/Drivers/Sierra.php +++ b/code/web/Drivers/Sierra.php @@ -1362,7 +1362,9 @@ public function findNewUser($patronBarcode, $patronUsername) { } $forceDisplayNameUpdate = false; - $primaryName = reset($patronInfo->names); + if ($patronInfo->names != null) { + $primaryName = reset($patronInfo->names); + } if (strpos($primaryName, ',') !== false) { [ $lastName, diff --git a/code/web/migrations/importBiblioCommonsData.php b/code/web/migrations/importBiblioCommonsData.php index 12987890d4..0f7d268305 100644 --- a/code/web/migrations/importBiblioCommonsData.php +++ b/code/web/migrations/importBiblioCommonsData.php @@ -312,9 +312,24 @@ function getGroupedWorkForRecordId($bibNumber, &$validRecords, &$invalidRecords) } elseif (array_key_exists($bibNumber, $invalidRecords)) { return null; } else { + $ils = ''; $groupedWorkPrimaryIdentifier = new GroupedWorkPrimaryIdentifier(); $groupedWorkPrimaryIdentifier->type = 'ils'; - $groupedWorkPrimaryIdentifier->identifier = $bibNumber; + $accountProfiles = new AccountProfile(); + $accountProfiles->find(); + while ($accountProfiles->fetch()) { + if ($accountProfiles->ils != 'na') { + $ils = $accountProfiles->ils; + } + } + if ($ils == 'sierra') { + $escapedFilter = $groupedWorkPrimaryIdentifier->escape('.b' . $bibNumber . '%'); + $groupedWorkPrimaryIdentifier->whereAdd("identifier LIKE $escapedFilter"); + + } else { + $groupedWorkPrimaryIdentifier->identifier = $bibNumber; + } + if ($groupedWorkPrimaryIdentifier->find(true)) { $groupedWork = new GroupedWork(); $groupedWork->id = $groupedWorkPrimaryIdentifier->grouped_work_id; diff --git a/code/web/release_notes/24.08.01.MD b/code/web/release_notes/24.08.01.MD index cdc7953044..a8d08fb761 100644 --- a/code/web/release_notes/24.08.01.MD +++ b/code/web/release_notes/24.08.01.MD @@ -2,6 +2,10 @@ ### Koha Updates - Changed data fetch for message_queue table to only query rows created in the last 24 hours. (*KK*) +### Other Updates +- Fixed issue where importing BiblioCommons data for Sierra libraries wasn't working correctly. (Ticket 131179) (*KL*) + ## This release includes code contributions from - ByWater Solutions - - Kirstien Kroeger (KK) \ No newline at end of file + - Kirstien Kroeger (KK) + - Kodi Lein (KL) \ No newline at end of file From 44b8ffcbabddb37176b8a9ed712a49ea984d36a0 Mon Sep 17 00:00:00 2001 From: Mark Noble Date: Thu, 22 Aug 2024 14:35:36 -0600 Subject: [PATCH 02/56] Create admin form for controlling format sort order --- .../interface/themes/responsive/js/aspen.js | 45 +++ .../themes/responsive/js/aspen/admin.js | 45 +++ .../Admin/GroupedWorkFormatSorting.php | 87 +++++ code/web/sys/Account/User.php | 4 + code/web/sys/DB/DataObject.php | 4 +- .../version_updates/24.09.00.php | 34 ++ .../Grouping/GroupedWorkDisplaySetting.php | 1 + .../sys/Grouping/GroupedWorkFormatSort.php | 57 +++ .../GroupedWorkFormatSortingGroup.php | 363 ++++++++++++++++++ 9 files changed, 638 insertions(+), 2 deletions(-) create mode 100644 code/web/services/Admin/GroupedWorkFormatSorting.php create mode 100644 code/web/sys/Grouping/GroupedWorkFormatSort.php create mode 100644 code/web/sys/Grouping/GroupedWorkFormatSortingGroup.php diff --git a/code/web/interface/themes/responsive/js/aspen.js b/code/web/interface/themes/responsive/js/aspen.js index 8cfafab3d3..1450683fed 100644 --- a/code/web/interface/themes/responsive/js/aspen.js +++ b/code/web/interface/themes/responsive/js/aspen.js @@ -9604,6 +9604,51 @@ AspenDiscovery.Admin = (function () { $("#propertyRowshowSearchToolsAtTop").hide(); } }, + initializeFormatSort: function () { + this.updateGroupedWorkSortFields('book'); + this.updateGroupedWorkSortFields('comic'); + this.updateGroupedWorkSortFields('movie'); + this.updateGroupedWorkSortFields('music'); + this.updateGroupedWorkSortFields('other'); + }, + updateGroupedWorkSortFields: function(groupingCategory) { + if (groupingCategory == 'book') { + var selectedOption = $("#bookSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedBookFormats").hide(); + }else{ + $("#propertyRowsortedBookFormats").show(); + } + }else if (groupingCategory == 'comic') { + var selectedOption = $("#comicSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedComicFormats").hide(); + }else{ + $("#propertyRowsortedComicFormats").show(); + } + }else if (groupingCategory == 'movie') { + var selectedOption = $("#movieSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedMovieFormats").hide(); + }else{ + $("#propertyRowsortedMovieFormats").show(); + } + }else if (groupingCategory == 'music') { + var selectedOption = $("#musicSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedMusicFormats").hide(); + }else{ + $("#propertyRowsortedMusicFormats").show(); + } + }else if (groupingCategory == 'other') { + var selectedOption = $("#otherSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedOtherFormats").hide(); + }else{ + $("#propertyRowsortedOtherFormats").show(); + } + } + }, updateIndexingProfileFields: function () { var audienceType = $('#determineAudienceBySelect').val(); if (audienceType === '3') { diff --git a/code/web/interface/themes/responsive/js/aspen/admin.js b/code/web/interface/themes/responsive/js/aspen/admin.js index 78b36b61e9..c69324dd63 100644 --- a/code/web/interface/themes/responsive/js/aspen/admin.js +++ b/code/web/interface/themes/responsive/js/aspen/admin.js @@ -1388,6 +1388,51 @@ AspenDiscovery.Admin = (function () { $("#propertyRowshowSearchToolsAtTop").hide(); } }, + initializeFormatSort: function () { + this.updateGroupedWorkSortFields('book'); + this.updateGroupedWorkSortFields('comic'); + this.updateGroupedWorkSortFields('movie'); + this.updateGroupedWorkSortFields('music'); + this.updateGroupedWorkSortFields('other'); + }, + updateGroupedWorkSortFields: function(groupingCategory) { + if (groupingCategory == 'book') { + var selectedOption = $("#bookSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedBookFormats").hide(); + }else{ + $("#propertyRowsortedBookFormats").show(); + } + }else if (groupingCategory == 'comic') { + var selectedOption = $("#comicSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedComicFormats").hide(); + }else{ + $("#propertyRowsortedComicFormats").show(); + } + }else if (groupingCategory == 'movie') { + var selectedOption = $("#movieSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedMovieFormats").hide(); + }else{ + $("#propertyRowsortedMovieFormats").show(); + } + }else if (groupingCategory == 'music') { + var selectedOption = $("#musicSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedMusicFormats").hide(); + }else{ + $("#propertyRowsortedMusicFormats").show(); + } + }else if (groupingCategory == 'other') { + var selectedOption = $("#otherSortMethodSelect").find(":selected").val(); + if (selectedOption == 1) { + $("#propertyRowsortedOtherFormats").hide(); + }else{ + $("#propertyRowsortedOtherFormats").show(); + } + } + }, updateIndexingProfileFields: function () { var audienceType = $('#determineAudienceBySelect').val(); if (audienceType === '3') { diff --git a/code/web/services/Admin/GroupedWorkFormatSorting.php b/code/web/services/Admin/GroupedWorkFormatSorting.php new file mode 100644 index 0000000000..5a7a14c999 --- /dev/null +++ b/code/web/services/Admin/GroupedWorkFormatSorting.php @@ -0,0 +1,87 @@ +orderBy($this->getSort()); + $this->applyFilters($object); + $object->limit(($page - 1) * $recordsPerPage, $recordsPerPage); + if (!UserAccount::userHasPermission('Administer All Format Sorting')) { + $library = Library::getPatronHomeLibrary(UserAccount::getActiveUserObj()); + $groupedWorkDisplaySettings = new GroupedWorkDisplaySetting(); + $groupedWorkDisplaySettings->id = $library->groupedWorkDisplaySettingId; + $groupedWorkDisplaySettings->find(true); + $object->id = $groupedWorkDisplaySettings->formatSortingGroupId; + } + $object->find(); + $list = []; + while ($object->fetch()) { + $list[$object->id] = clone $object; + } + return $list; + } + + function getDefaultSort(): string { + return 'name asc'; + } + + function getObjectStructure($context = ''): array { + return GroupedWorkFormatSortingGroup::getObjectStructure($context); + } + + function getPrimaryKeyColumn(): string { + return 'id'; + } + + function getIdKeyColumn(): string { + return 'id'; + } + + function getInstructions(): string { + return ''; + } + + function getBreadcrumbs(): array { + $breadcrumbs = []; + $breadcrumbs[] = new Breadcrumb('/Admin/Home', 'Administration Home'); + $breadcrumbs[] = new Breadcrumb('/Admin/Home#cataloging', 'Catalog / Grouped Works'); + $breadcrumbs[] = new Breadcrumb('/Admin/GroupedWorkFormatSorting', 'Grouped Work Format Sorting'); + return $breadcrumbs; + } + + function getActiveAdminSection(): string { + return 'cataloging'; + } + + function canView(): bool { + return UserAccount::userHasPermission([ + 'Administer All Format Sorting', + 'Administer Library Format Sorting', + ]); + } + + function canBatchEdit(): bool { + return UserAccount::userHasPermission([ + 'Administer All Format Sorting', + ]); + } + + function getInitializationJs(): string { + return 'AspenDiscovery.Admin.initializeFormatSort();'; + } +} \ No newline at end of file diff --git a/code/web/sys/Account/User.php b/code/web/sys/Account/User.php index efdaa7120f..f78591e686 100644 --- a/code/web/sys/Account/User.php +++ b/code/web/sys/Account/User.php @@ -3459,6 +3459,10 @@ public function getAdminActions() { 'Administer All Grouped Work Facets', 'Administer Library Grouped Work Facets', ]); + $groupedWorkAction->addSubAction(new AdminAction('Format Sorting', 'Define how formats are sorted within a Grouped Work.', '/Admin/GroupedWorkFormatSorting'), [ + 'Administer All Format Sorting', + 'Administer Library Format Sorting', + ]); $sections['cataloging']->addAction($groupedWorkAction, [ 'Administer All Grouped Work Display Settings', 'Administer Library Grouped Work Display Settings', diff --git a/code/web/sys/DB/DataObject.php b/code/web/sys/DB/DataObject.php index 298603b117..9683f4b503 100644 --- a/code/web/sys/DB/DataObject.php +++ b/code/web/sys/DB/DataObject.php @@ -965,7 +965,7 @@ public function setProperty(string $propertyName, $newValue, ?array $propertyStr if ($propertyChanged) { $this->_changedFields[] = $propertyName; $oldValue = $this->$propertyName; - if ($propertyStructure['type'] == 'checkbox') { + if ($propertyStructure != null && $propertyStructure['type'] == 'checkbox') { if ($newValue == 'off' || $newValue == false) { $newValue = 0; } elseif ($newValue == 'on' || $newValue == true) { @@ -980,7 +980,7 @@ public function setProperty(string $propertyName, $newValue, ?array $propertyStr $logger->log("Forcing Nightly Index because $propertyName on " . get_class($this) . ' - ' . $this->getPrimaryKeyValue() . " was changed to $newValue by user " . UserAccount::getActiveUserId(), Logger::LOG_ALERT); } //Add the change to the history unless tracking the history is off (passwords) - if ($propertyStructure['type'] != 'password' && $propertyStructure['type'] != 'storedPassword') { + if ($propertyStructure != null && $propertyStructure['type'] != 'password' && $propertyStructure['type'] != 'storedPassword') { if ($this->objectHistoryEnabled()) { require_once ROOT_DIR . '/sys/DB/DataObjectHistory.php'; $history = new DataObjectHistory(); diff --git a/code/web/sys/DBMaintenance/version_updates/24.09.00.php b/code/web/sys/DBMaintenance/version_updates/24.09.00.php index 6dad2af123..5eb367c50a 100644 --- a/code/web/sys/DBMaintenance/version_updates/24.09.00.php +++ b/code/web/sys/DBMaintenance/version_updates/24.09.00.php @@ -66,6 +66,40 @@ function getUpdates24_09_00(): array { "INSERT INTO role_permissions(roleId, permissionId) VALUES ((SELECT roleId from roles where name='opacAdmin'), (SELECT id from permissions where name='Test Self Check'))", ] ], //add_permission_for_testing_checkouts + 'add_permission_for_format_sorting' => [ + 'title' => 'Add permissions for format sorting', + 'description' => 'Add permissions for format sorting', + 'continueOnError' => false, + 'sql' => [ + "INSERT INTO permissions (sectionName, name, requiredModule, weight, description) VALUES ('Grouped Work Display', 'Administer All Format Sorting', '', 40, 'Allows users to change how formats are sorted within a grouped work for all libraries.')", + "INSERT INTO permissions (sectionName, name, requiredModule, weight, description) VALUES ('Grouped Work Display', 'Administer Library Format Sorting', '', 50, 'Allows users to change how formats are sorted within a grouped work for their library.')", + "INSERT INTO role_permissions(roleId, permissionId) VALUES ((SELECT roleId from roles where name='opacAdmin'), (SELECT id from permissions where name='Administer All Format Sorting'))", + ] + ], //add_permission_for_format_sorting + 'create_format_sorting_tables' => [ + 'title' => 'Create format sorting tables', + 'description' => 'Create format sorting tables', + 'continueOnError' => true, + 'sql' => [ + 'CREATE TABLE IF NOT EXISTS grouped_work_format_sort_group ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + bookSortMethod TINYINT(1) DEFAULT 1, + comicSortMethod TINYINT(1) DEFAULT 1, + movieSortMethod TINYINT(1) DEFAULT 1, + musicSortMethod TINYINT(1) DEFAULT 1, + otherSortMethod TINYINT(1) DEFAULT 1 + )', + 'CREATE TABLE IF NOT EXISTS grouped_work_format_sort ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + formatSortingGroupId INT(11) NOT NULL, + groupingCategory VARCHAR(6) NOT NULL, + format VARCHAR(255) NOT NULL, + weight INT(11) NOT NULL, + UNIQUE(formatSortingGroupId, groupingCategory, format) + )', + ] + ], //create_format_sorting_tables //katherine - ByWater diff --git a/code/web/sys/Grouping/GroupedWorkDisplaySetting.php b/code/web/sys/Grouping/GroupedWorkDisplaySetting.php index ae97820454..0f816ab2bc 100644 --- a/code/web/sys/Grouping/GroupedWorkDisplaySetting.php +++ b/code/web/sys/Grouping/GroupedWorkDisplaySetting.php @@ -47,6 +47,7 @@ class GroupedWorkDisplaySetting extends DataObject { public $includeAllRecordsInDateAddedFacets; public $facetCountsToShow; public $facetGroupId; + public $formatSortingGroupId; //Enrichment public $showStandardReviews; diff --git a/code/web/sys/Grouping/GroupedWorkFormatSort.php b/code/web/sys/Grouping/GroupedWorkFormatSort.php new file mode 100644 index 0000000000..259c48f104 --- /dev/null +++ b/code/web/sys/Grouping/GroupedWorkFormatSort.php @@ -0,0 +1,57 @@ + [ + 'property' => 'id', + 'type' => 'label', + 'label' => 'Id', + 'description' => 'The unique id within the database', + ], + 'weight' => [ + 'property' => 'weight', + 'type' => 'numeric', + 'label' => 'Weight', + 'weight' => 'Defines how items are sorted. Lower weights are displayed higher.', + 'required' => true, + ], + 'formatSortingGroupId' => [ + 'property' => 'formatSortingGroupId', + 'type' => 'integer', + 'label' => 'Format Sorting Group ID', + 'description' => 'The sorting group this belongs to', + ], + 'groupingCategory' => [ + 'property' => 'groupingCategory', + 'type' => 'enum', + 'values' => [ + 'book' => 'book', + 'comic' => 'comic', + 'movie' => 'movie', + 'music' => 'music', + 'other' => 'other' + ], + 'label' => 'Grouping Category', + 'description' => 'The category this format belongs in', + ], + 'format' => [ + 'property' => 'format', + 'type' => 'text', + 'label' => 'Format', + 'description' => 'The format being sorted', + 'size' => '40', + 'maxLength' => 255, + 'readOnly' => true, + ], + ]; + } +} \ No newline at end of file diff --git a/code/web/sys/Grouping/GroupedWorkFormatSortingGroup.php b/code/web/sys/Grouping/GroupedWorkFormatSortingGroup.php new file mode 100644 index 0000000000..269ca52c2f --- /dev/null +++ b/code/web/sys/Grouping/GroupedWorkFormatSortingGroup.php @@ -0,0 +1,363 @@ + [ + 'property' => 'id', + 'type' => 'label', + 'label' => 'Id', + 'description' => 'The unique id within the database', + ], + 'name' => [ + 'property' => 'name', + 'type' => 'text', + 'label' => 'Display Name', + 'description' => 'The name of the settings', + 'size' => '40', + 'maxLength' => 255, + ], + 'booksSection' => [ + 'property' => 'booksSection', + 'type' => 'section', + 'label' => 'Books', + 'properties' => [ + 'bookSortMethod' => [ + 'property' => 'bookSortMethod', + 'type' => 'enum', + 'values' => [ + '1' => 'Sort Alphabetically with Books first', + '2' => 'Custom Sort' + ], + 'label' => 'Sorting Method for Books', + 'description' => 'Determines how formats are sorted for grouped works with a grouping category of book', + 'onchange' => "return AspenDiscovery.Admin.updateGroupedWorkSortFields('book');", + ], + 'sortedBookFormats' => [ + 'property' => 'sortedBookFormats', + 'type' => 'oneToMany', + 'label' => 'Sorted Book Formats', + 'description' => 'A list of formats in the order they should be displayed', + 'keyThis' => 'id', + 'keyOther' => 'formatSortingGroupId', + 'subObjectType' => 'GroupedWorkFormatSort', + 'structure' => $formatSortStructure, + 'sortable' => true, + 'storeDb' => true, + 'allowEdit' => false, + 'canEdit' => false, + 'canAddNew' => false, + 'canDelete' => false, + ], + ] + ], + + 'comicsSection' => [ + 'property' => 'comicsSection', + 'type' => 'section', + 'label' => 'Comics', + 'properties' => [ + 'comicSortMethod' => [ + 'property' => 'comicSortMethod', + 'type' => 'enum', + 'values' => [ + '1' => 'Sort Alphabetically', + '2' => 'Custom Sort' + ], + 'label' => 'Sorting Method for Comics', + 'description' => 'Determines how formats are sorted for grouped works with a grouping category of comic', + 'onchange' => "return AspenDiscovery.Admin.updateGroupedWorkSortFields('comic');", + ], + 'sortedComicFormats' => [ + 'property' => 'sortedComicFormats', + 'type' => 'oneToMany', + 'label' => 'Sorted Comic Formats', + 'description' => 'A list of formats in the order they should be displayed', + 'keyThis' => 'id', + 'keyOther' => 'formatSortingGroupId', + 'subObjectType' => 'GroupedWorkFormatSort', + 'structure' => $formatSortStructure, + 'sortable' => true, + 'storeDb' => true, + 'allowEdit' => false, + 'canEdit' => false, + 'canAddNew' => false, + 'canDelete' => false, + ], + ], + ], + 'moviesSection' => [ + 'property' => 'moviesSection', + 'type' => 'section', + 'label' => 'Movies', + 'properties' => [ + 'movieSortMethod' => [ + 'property' => 'movieSortMethod', + 'type' => 'enum', + 'values' => [ + '1' => 'Sort Alphabetically', + '2' => 'Custom Sort' + ], + 'label' => 'Sorting Method for Movies', + 'description' => 'Determines how formats are sorted for grouped works with a grouping category of movie', + 'onchange' => "return AspenDiscovery.Admin.updateGroupedWorkSortFields('movie');", + ], + 'sortedMovieFormats' => [ + 'property' => 'sortedMovieFormats', + 'type' => 'oneToMany', + 'label' => 'Sorted Movie Formats', + 'description' => 'A list of formats in the order they should be displayed', + 'keyThis' => 'id', + 'keyOther' => 'formatSortingGroupId', + 'subObjectType' => 'GroupedWorkFormatSort', + 'structure' => $formatSortStructure, + 'sortable' => true, + 'storeDb' => true, + 'allowEdit' => false, + 'canEdit' => false, + 'canAddNew' => false, + 'canDelete' => false, + ], + ], + ], + 'musicSection' => [ + 'property' => 'musicSection', + 'type' => 'section', + 'label' => 'Music', + 'properties' => [ + 'musicSortMethod' => [ + 'property' => 'musicSortMethod', + 'type' => 'enum', + 'values' => [ + '1' => 'Sort Alphabetically', + '2' => 'Custom Sort' + ], + 'label' => 'Sorting Method for Music', + 'description' => 'Determines how formats are sorted for grouped works with a grouping category of music', + 'onchange' => "return AspenDiscovery.Admin.updateGroupedWorkSortFields('music');", + ], + 'sortedMusicFormats' => [ + 'property' => 'sortedMusicFormats', + 'type' => 'oneToMany', + 'label' => 'Sorted Music Formats', + 'description' => 'A list of formats in the order they should be displayed', + 'keyThis' => 'id', + 'keyOther' => 'formatSortingGroupId', + 'subObjectType' => 'GroupedWorkFormatSort', + 'structure' => $formatSortStructure, + 'sortable' => true, + 'storeDb' => true, + 'allowEdit' => false, + 'canEdit' => false, + 'canAddNew' => false, + 'canDelete' => false, + ], + ], + ], + 'otherSection' => [ + 'property' => 'otherSection', + 'type' => 'section', + 'label' => 'Other', + 'properties' => [ + 'otherSortMethod' => [ + 'property' => 'otherSortMethod', + 'type' => 'enum', + 'values' => [ + '1' => 'Sort Alphabetically', + '2' => 'Custom Sort' + ], + 'label' => 'Sorting Method for Other', + 'description' => 'Determines how formats are sorted for grouped works with a grouping category of other', + 'onchange' => "return AspenDiscovery.Admin.updateGroupedWorkSortFields('other');", + ], + 'sortedOtherFormats' => [ + 'property' => 'sortedOtherFormats', + 'type' => 'oneToMany', + 'label' => 'Sorted Other Formats', + 'description' => 'A list of formats in the order they should be displayed', + 'keyThis' => 'id', + 'keyOther' => 'formatSortingGroupId', + 'subObjectType' => 'GroupedWorkFormatSort', + 'structure' => $formatSortStructure, + 'sortable' => true, + 'storeDb' => true, + 'allowEdit' => false, + 'canEdit' => false, + 'canAddNew' => false, + 'canDelete' => false, + ], + ], + ], + ]; + if ($context == 'addNew') { + unset($objectStructure['booksSection']); + unset($objectStructure['comicsSection']); + unset($objectStructure['moviesSection']); + unset($objectStructure['musicSection']); + unset($objectStructure['otherSection']); + } + return $objectStructure; + } + + public function update($context = '') { + $ret = parent::update(); + if ($ret !== FALSE) { + $this->saveSortedFormats('book'); + $this->saveSortedFormats('comic'); + $this->saveSortedFormats('movie'); + $this->saveSortedFormats('music'); + $this->saveSortedFormats('other'); + } + return $ret; + } + + public function insert($context = '') { + $ret = parent::insert(); + if ($ret !== FALSE) { + $this->loadDefaultFormats(); + } + return $ret; + } + + public function delete($useWhere = false): int { + $ret = parent::delete($useWhere); + if ($ret !== false) { + $sortedFormat = new GroupedWorkFormatSort(); + $sortedFormat->formatSortingGroupId = $this->id; + $sortedFormat->delete(true); + } + return $ret; + } + + private function getArrayNameForGroupingCategory($groupingCategory) { + $internalArrayName = null; + switch ($groupingCategory) { + case 'book': + $internalArrayName = '_sortedBookFormats'; + break; + case 'comic': + $internalArrayName = '_sortedComicFormats'; + break; + case 'movie': + $internalArrayName = '_sortedMovieFormats'; + break; + case 'music': + $internalArrayName = '_sortedMusicFormats'; + break; + case 'other': + $internalArrayName = '_sortedOtherFormats'; + break; + } + return $internalArrayName; + } + + public function saveSortedFormats($groupingCategory) { + $internalArrayName = $this->getArrayNameForGroupingCategory($groupingCategory); + if (!empty($internalArrayName) && isset ($this->$internalArrayName) && is_array($this->$internalArrayName)) { + foreach ($this->$internalArrayName as $id => $formatSort) { + $formatSort->groupingCategory = $groupingCategory; + } + $this->saveOneToManyOptions($this->$internalArrayName, 'formatSortingGroupId'); + unset($this->$internalArrayName); + } + } + + public function __get($name) { + if ($name == 'sortedBookFormats') { + return $this->getSortedFormats('book'); + } elseif ($name == 'sortedComicFormats') { + return $this->getSortedFormats('comic'); + } elseif ($name == 'sortedMovieFormats') { + return $this->getSortedFormats('movie'); + } elseif ($name == 'sortedMusicFormats') { + return $this->getSortedFormats('music'); + } elseif ($name == 'sortedOtherFormats') { + return $this->getSortedFormats('other'); + } else { + return parent::__get($name); + } + } + + public function __set($name, $value) { + if ($name == 'sortedBookFormats') { + $this->setSortedFormats('book', $value); + } elseif ($name == 'sortedComicFormats') { + $this->setSortedFormats('comic', $value); + } elseif ($name == 'sortedMovieFormats') { + $this->setSortedFormats('movie', $value); + } elseif ($name == 'sortedMusicFormats') { + $this->setSortedFormats('music', $value); + } elseif ($name == 'sortedOtherFormats') { + $this->setSortedFormats('other', $value); + } else { + parent::__set($name, $value); + } + } + + /** @return GroupedWorkFormatSort[] */ + public function getSortedFormats($groupingCategory): ?array { + $internalArrayName = $this->getArrayNameForGroupingCategory($groupingCategory); + if (!empty($internalArrayName) && !isset($this->$internalArrayName) && $this->id) { + $this->$internalArrayName = []; + $sortedFormat = new GroupedWorkFormatSort(); + $sortedFormat->facetGroupId = $this->id; + $sortedFormat->groupingCategory = $groupingCategory; + $sortedFormat->orderBy('weight'); + $sortedFormat->find(); + while ($sortedFormat->fetch()) { + $this->$internalArrayName[$sortedFormat->id] = clone($sortedFormat); + } + } + return $this->$internalArrayName; + } + + private function loadDefaultFormats() { + //Automatically generate based on the data in the database. + global $aspen_db; + $loadDefaultFormatsStmt = "SELECT grouping_category, format FROM grouped_work_variation inner join indexed_format on indexed_format.id = formatId inner join grouped_work on grouped_work.id = grouped_work_variation.groupedWorkId group by format, grouping_category order by grouping_category, lower(format);"; + $results = $aspen_db->query($loadDefaultFormatsStmt, PDO::FETCH_ASSOC); + $weight = 1; + $lastGroupingCategory = null; + foreach ($results as $result) { + if ($lastGroupingCategory != $result['grouping_category']) { + $lastGroupingCategory = $result['grouping_category']; + $weight = 1; + } + $groupedWorkFormatSort = new GroupedWorkFormatSort(); + $groupedWorkFormatSort->formatSortingGroupId = $this->id; + $groupedWorkFormatSort->groupingCategory = $result['grouping_category']; + $groupedWorkFormatSort->format = $result['format']; + $groupedWorkFormatSort->weight = $weight++; + $groupedWorkFormatSort->insert(); + } + } + + private function setSortedFormats(string $groupingCategory, ?array $value) { + $internalArrayName = $this->getArrayNameForGroupingCategory($groupingCategory); + if (!empty($internalArrayName)) { + $this->$internalArrayName = $value; + } + } + +} \ No newline at end of file From ac98fa4df0e8b194ce2c3dd219703225eeced1ff Mon Sep 17 00:00:00 2001 From: Kodi Date: Thu, 22 Aug 2024 14:02:23 -0700 Subject: [PATCH 03/56] Koha Volume Level Holds Update volume level hold functionality for Aspen->Koha to not include an item group id if there is no item group id (if it is 0) Update release notes --- code/web/Drivers/Koha.php | 20 ++++++++++++++------ code/web/release_notes/24.08.01.MD | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/code/web/Drivers/Koha.php b/code/web/Drivers/Koha.php index e5a399182b..7c6456c9bb 100644 --- a/code/web/Drivers/Koha.php +++ b/code/web/Drivers/Koha.php @@ -2010,12 +2010,20 @@ public function placeVolumeHold(User $patron, $recordId, $volumeId, $pickupBranc } else { $apiUrl = $this->getWebServiceUrl() . "/api/v1/holds"; if ($this->getKohaVersion() >= 22.11) { - $postParams = [ - 'patron_id' => $patron->unique_ils_id, - 'pickup_library_id' => $pickupBranch, - 'item_group_id' => (int)$volumeId, - 'biblio_id' => $recordId, - ]; + if ($volumeId != 0){ + $postParams = [ + 'patron_id' => $patron->unique_ils_id, + 'pickup_library_id' => $pickupBranch, + 'item_group_id' => (int)$volumeId, + 'biblio_id' => $recordId, + ]; + } else { //if there is no item group id + $postParams = [ + 'patron_id' => $patron->unique_ils_id, + 'pickup_library_id' => $pickupBranch, + 'biblio_id' => $recordId, + ]; + } } else { $postParams = [ 'patron_id' => $patron->unique_ils_id, diff --git a/code/web/release_notes/24.08.01.MD b/code/web/release_notes/24.08.01.MD index a8d08fb761..96c91d5a02 100644 --- a/code/web/release_notes/24.08.01.MD +++ b/code/web/release_notes/24.08.01.MD @@ -1,6 +1,7 @@ ## Aspen Discovery Updates ### Koha Updates - Changed data fetch for message_queue table to only query rows created in the last 24 hours. (*KK*) +- Updated placing volume holds to not pass in an item group id if none exists for the volume (Ticket 137232) (*KL*) ### Other Updates - Fixed issue where importing BiblioCommons data for Sierra libraries wasn't working correctly. (Ticket 131179) (*KL*) From 0f47f01e562dd890c46f0e87e318f0acba1f1792 Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Thu, 22 Aug 2024 17:59:19 -0500 Subject: [PATCH 04/56] check deleteAlternateLibraryCard for false as a string --- code/web/services/API/UserAPI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/web/services/API/UserAPI.php b/code/web/services/API/UserAPI.php index 2de85af829..4094d3415c 100644 --- a/code/web/services/API/UserAPI.php +++ b/code/web/services/API/UserAPI.php @@ -6062,7 +6062,7 @@ function updateAlternateLibraryCard(): array { $alternateLibraryCard = $_REQUEST['alternateLibraryCard'] ?? null; $alternateLibraryCardPassword = $_REQUEST['alternateLibraryCardPassword'] ?? null; $deleteAlternateLibraryCard = $_REQUEST['deleteAlternateLibraryCard'] ?? false; - if(!$deleteAlternateLibraryCard) { + if(!$deleteAlternateLibraryCard || $deleteAlternateLibraryCard === "false") { if ($alternateLibraryCard) { $user->alternateLibraryCard = $alternateLibraryCard; } From 7db1c58958e9eddf2a460c70e2fb10569799599d Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Thu, 22 Aug 2024 18:30:55 -0500 Subject: [PATCH 05/56] add alternate library cards to LiDA --- .../src/components/Action/ActionButton.js | 8 + .../Action/AddAlternateLibraryCard.js | 223 ++++++++++ .../components/Action/CheckOut/CheckOut.js | 149 ++++++- .../src/components/Action/Holds/HoldPrompt.js | 387 +++++++++++++++--- .../src/components/Action/Holds/PlaceHold.js | 4 +- .../src/navigations/drawer/DrawerContent.js | 34 ++ .../stack/LibraryCardStackNavigator.js | 8 + .../src/screens/GroupedWork/Editions.js | 42 +- .../src/screens/GroupedWork/Variations.js | 36 ++ .../MyLibraryCard/MyAlternateLibraryCard.js | 186 +++++++++ .../MyAccount/MyLibraryCard/MyLibraryCard.js | 26 ++ code/aspen_app/src/translations/defaults.json | 5 +- code/aspen_app/src/util/api/user.js | 36 ++ code/web/release_notes/24.09.00.MD | 2 + 14 files changed, 1082 insertions(+), 64 deletions(-) create mode 100644 code/aspen_app/src/components/Action/AddAlternateLibraryCard.js create mode 100644 code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js diff --git a/code/aspen_app/src/components/Action/ActionButton.js b/code/aspen_app/src/components/Action/ActionButton.js index 4ce971e257..ea439049d8 100644 --- a/code/aspen_app/src/components/Action/ActionButton.js +++ b/code/aspen_app/src/components/Action/ActionButton.js @@ -41,6 +41,8 @@ export const ActionButton = (data) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + userHasAlternateLibraryCard, + shouldPromptAlternateLibraryCard, } = data; if (_.isObject(action)) { if (action.type === 'overdrive_sample') { @@ -81,6 +83,9 @@ export const ActionButton = (data) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} + recordSource={recordSource} /> ); } else if (action.type === 'vdx_request') { @@ -136,6 +141,9 @@ export const ActionButton = (data) => { cancelHoldConfirmationRef={cancelHoldConfirmationRef} holdConfirmationResponse={holdConfirmationResponse} setHoldConfirmationResponse={setHoldConfirmationResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} + recordSource={recordSource} /> ); } diff --git a/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js b/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js new file mode 100644 index 0000000000..92f56cfa15 --- /dev/null +++ b/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js @@ -0,0 +1,223 @@ +import React from 'react'; +import _ from 'lodash'; +import { CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; +import { LanguageContext, LibrarySystemContext, ThemeContext, UserContext } from '../../context/initialContext'; +import { getTermFromDictionary } from '../../translations/TranslationService'; +import { refreshProfile, updateAlternateLibraryCard } from '../../util/api/user'; +import { decodeHTML } from '../../util/apiAuth'; +import { completeAction } from '../../util/recordActions'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; +import { EyeOff, Eye } from 'lucide-react-native'; + +export const AddAlternateLibraryCard = (props) => { + const { + id, + title, + action, + volumeInfo, + holdTypeForFormat, + variationId, + prevRoute, + isEContent, + response, + setResponse, + responseIsOpen, + setResponseIsOpen, + onResponseClose, + cancelResponseRef, + holdConfirmationResponse, + setHoldConfirmationResponse, + holdConfirmationIsOpen, + setHoldConfirmationIsOpen, + onHoldConfirmationClose, + cancelHoldConfirmationRef, + holdSelectItemResponse, + setHoldSelectItemResponse, + holdItemSelectIsOpen, + setHoldItemSelectIsOpen, + onHoldItemSelectClose, + cancelHoldItemSelectRef, + recordSource, + activeAccount, + } = props; + + let isPlacingHold = false; + if (_.isObject(action)) { + isPlacingHold = action.includes('hold'); + } + + const { library } = React.useContext(LibrarySystemContext); + const { user, updateUser } = React.useContext(UserContext); + const { language } = React.useContext(LanguageContext); + const { theme, textColor, colorMode } = React.useContext(ThemeContext); + const queryClient = useQueryClient(); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showModal, setShowModal] = React.useState(true); + const [loading, setLoading] = React.useState(false); + + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + }; + + return ( + setShowModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {isPlacingHold ? getTermFromDictionary(language, 'hold_options') : getTermFromDictionary(language, 'checkout_options')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/code/aspen_app/src/components/Action/CheckOut/CheckOut.js b/code/aspen_app/src/components/Action/CheckOut/CheckOut.js index ae467b92b2..594d1978e1 100644 --- a/code/aspen_app/src/components/Action/CheckOut/CheckOut.js +++ b/code/aspen_app/src/components/Action/CheckOut/CheckOut.js @@ -1,23 +1,27 @@ -import { Box, Button, ButtonSpinner, ButtonGroup, ButtonIcon, ButtonText, Text } from '@gluestack-ui/themed'; +import { Box, Button, ButtonSpinner, ButtonGroup, ButtonIcon, ButtonText, Text, Heading, Icon, CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; import React from 'react'; import _ from 'lodash'; import { useQueryClient } from '@tanstack/react-query'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; // custom components and helper files import { LanguageContext, LibraryBranchContext, LibrarySystemContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { decodeHTML } from '../../../util/apiAuth'; import { completeAction } from '../../../util/recordActions'; -import { refreshProfile } from '../../../util/api/user'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; import { HoldPrompt } from '../Holds/HoldPrompt'; import { getTermFromDictionary } from '../../../translations/TranslationService'; export const CheckOut = (props) => { const queryClient = useQueryClient(); - const { id, title, type, record, prevRoute, response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef } = props; + const { id, title, type, record, prevRoute, response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, userHasAlternateLibraryCard, shouldPromptAlternateLibraryCard } = props; const { user, updateUser, accounts } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); const { language } = React.useContext(LanguageContext); const [loading, setLoading] = React.useState(false); - const { theme } = React.useContext(ThemeContext); + const { theme, colorMode, textColor } = React.useContext(ThemeContext); const volumeInfo = { numItemsWithVolumes: 0, @@ -47,8 +51,145 @@ export const CheckOut = (props) => { cancelHoldConfirmationRef={cancelHoldConfirmationRef} holdConfirmationResponse={holdConfirmationResponse} setHoldConfirmationResponse={setHoldConfirmationResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> ); + } else if (shouldPromptAlternateLibraryCard && !userHasAlternateLibraryCard) { + const [showAddAlternateLibraryCardModal, setShowAddAlternateLibraryCardModal] = React.useState(false); + + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + return ( + <> + + setShowAddAlternateLibraryCardModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {getTermFromDictionary(language, 'add_alternate_library_card')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + + ); } else { return ( <> diff --git a/code/aspen_app/src/components/Action/Holds/HoldPrompt.js b/code/aspen_app/src/components/Action/Holds/HoldPrompt.js index 85a61cbee1..e8604ec715 100644 --- a/code/aspen_app/src/components/Action/Holds/HoldPrompt.js +++ b/code/aspen_app/src/components/Action/Holds/HoldPrompt.js @@ -1,9 +1,13 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import _ from 'lodash'; -import { CheckIcon, CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Select, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, SelectScrollView } from '@gluestack-ui/themed'; +import { CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Select, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, SelectScrollView, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; import React from 'react'; -import { Platform } from 'react-native'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; import { HoldsContext, LibrarySystemContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; +import { decodeHTML } from '../../../util/apiAuth'; import { completeAction } from '../../../util/recordActions'; import { getTermFromDictionary } from '../../../translations/TranslationService'; import { getCopies } from '../../../util/api/item'; @@ -41,9 +45,14 @@ export const HoldPrompt = (props) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + recordSource, } = props; + + const [userHasAlternateLibraryCard, setUserHasAlternateLibraryCard] = React.useState(props.userHasAlternateLibraryCard ?? false); + const [promptAlternateLibraryCard, setPromptAlternateLibraryCard] = React.useState(props.shouldPromptAlternateLibraryCard ?? false); const [loading, setLoading] = React.useState(false); const [showModal, setShowModal] = React.useState(false); + const [showAddAlternateLibraryCardModal, setShowAddAlternateLibraryCardModal] = React.useState(false); const { user, updateUser, accounts, locations } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); @@ -117,8 +126,115 @@ export const HoldPrompt = (props) => { const [volume, setVolume] = React.useState(''); const [item, setItem] = React.useState(''); + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + const [activeAccount, setActiveAccount] = React.useState(user.id ?? ''); + const updateActiveAccount = (newId) => { + setActiveAccount(newId); + if (newId !== user.id) { + let newAccount = _.filter(accounts, ['id', newId]); + if (newAccount[0]) { + newAccount = newAccount[0]; + + // we need to recalculate if the linked account is eligible for using alternate library cards + if (newAccount) { + if (typeof newAccount.alternateLibraryCard !== 'undefined') { + const alternateLibraryCardOptions = newAccount?.alternateLibraryCardOptions ?? []; + if (alternateLibraryCardOptions) { + if (alternateLibraryCardOptions.showAlternateLibraryCard === '1' || alternateLibraryCardOptions.showAlternateLibraryCard === 1) { + if (recordSource === 'cloud_library' && (alternateLibraryCardOptions.useAlternateLibraryCardForCloudLibrary === '1' || alternateLibraryCardOptions.useAlternateLibraryCardForCloudLibrary === 1)) { + setPromptAlternateLibraryCard(true); + } + } + + if (newAccount.alternateLibraryCard && newAccount.alternateLibraryCard !== '') { + if (alternateLibraryCardOptions?.showAlternateLibraryCardPassword === '1') { + if (newAccount.alternateLibraryCardPassword !== '') { + setUserHasAlternateLibraryCard(true); + } else { + setUserHasAlternateLibraryCard(false); + } + } else { + setUserHasAlternateLibraryCard(true); + } + } else { + setUserHasAlternateLibraryCard(false); + } + + if (alternateLibraryCardOptions?.alternateLibraryCardLabel) { + cardLabel = alternateLibraryCardOptions.alternateLibraryCardLabel; + } + + if (alternateLibraryCardOptions?.alternateLibraryCardPasswordLabel) { + passwordLabel = alternateLibraryCardOptions.alternateLibraryCardPasswordLabel; + } + + if (alternateLibraryCardOptions?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(alternateLibraryCardOptions.alternateLibraryCardFormMessage); + } + + if (alternateLibraryCardOptions?.showAlternateLibraryCardPassword) { + if (alternateLibraryCardOptions.showAlternateLibraryCardPassword === '1' || alternateLibraryCardOptions.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + } else { + setUserHasAlternateLibraryCard(false); + setPromptAlternateLibraryCard(false); + } + } else { + setUserHasAlternateLibraryCard(false); + setPromptAlternateLibraryCard(false); + } + } + } + } else { + //revert back to primary user id + setUserHasAlternateLibraryCard(props.userHasAlternateLibraryCard); + setPromptAlternateLibraryCard(props.shouldPromptAlternateLibraryCard); + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + } + }; + let userPickupLocationId = user.pickupLocationId ?? user.homeLocationId; if (_.isNumber(user.pickupLocationId)) { userPickupLocationId = _.toString(user.pickupLocationId); @@ -144,6 +260,154 @@ export const HoldPrompt = (props) => { const [location, setLocation] = React.useState(pickupLocation); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + + if (showAddAlternateLibraryCardModal) { + return ( + setShowAddAlternateLibraryCardModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {getTermFromDictionary(language, 'add_alternate_library_card')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + ); + } + return ( <> - + ) : ( + + }); + }}> + {loading ? : {title}} + + )} diff --git a/code/aspen_app/src/components/Action/Holds/PlaceHold.js b/code/aspen_app/src/components/Action/Holds/PlaceHold.js index 876fea5ca1..4e97cfe5ca 100644 --- a/code/aspen_app/src/components/Action/Holds/PlaceHold.js +++ b/code/aspen_app/src/components/Action/Holds/PlaceHold.js @@ -38,6 +38,8 @@ export const PlaceHold = (props) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + userHasAlternateLibraryCard, + shouldPromptAlternateLibraryCard, } = props; const { user, updateUser, accounts, locations } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); @@ -73,7 +75,7 @@ export const PlaceHold = (props) => { let promptForHoldNotifications = user.promptForHoldNotifications ?? false; let loadHoldPrompt = false; - if (volumeInfo.numItemsWithVolumes >= 1 || _.size(accounts) > 0 || _.size(locations) > 1 || promptForHoldNotifications || holdTypeForFormat === 'item' || holdTypeForFormat === 'either') { + if (volumeInfo.numItemsWithVolumes >= 1 || _.size(accounts) > 0 || _.size(locations) > 1 || promptForHoldNotifications || holdTypeForFormat === 'item' || holdTypeForFormat === 'either' || (shouldPromptAlternateLibraryCard && !userHasAlternateLibraryCard)) { loadHoldPrompt = true; } diff --git a/code/aspen_app/src/navigations/drawer/DrawerContent.js b/code/aspen_app/src/navigations/drawer/DrawerContent.js index db2c11a2f9..d312bef0e1 100644 --- a/code/aspen_app/src/navigations/drawer/DrawerContent.js +++ b/code/aspen_app/src/navigations/drawer/DrawerContent.js @@ -415,6 +415,7 @@ export const DrawerContent = () => { + @@ -811,6 +812,39 @@ const UserPreferences = () => { ); }; +const AlternateLibraryCard = () => { + const { library } = React.useContext(LibrarySystemContext); + const { language } = React.useContext(LanguageContext); + const version = formatDiscoveryVersion(library.discoveryVersion); + + let shouldShowAlternateLibraryCard = false; + if (typeof library.showAlternateLibraryCard !== 'undefined') { + shouldShowAlternateLibraryCard = library.showAlternateLibraryCard; + } + + if (version >= '24.09.00' && (shouldShowAlternateLibraryCard === '1' || shouldShowAlternateLibraryCard === 1)) { + return ( + { + navigateStack('LibraryCardTab', 'MyAlternateLibraryCard', { + prevRoute: 'AccountDrawer', + hasPendingChanges: false, + }); + }}> + + + {getTermFromDictionary(language, 'alternate_library_card')} + + + ); + } + + return null; +}; + const Fines = () => { const { user } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); diff --git a/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js b/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js index f51c4f27eb..534a62bb7d 100644 --- a/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js +++ b/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js @@ -1,5 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; +import { MyAlternateLibraryCard } from '../../screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard'; import { MyLibraryCard } from '../../screens/MyAccount/MyLibraryCard/MyLibraryCard'; import { LanguageContext, LibrarySystemContext } from '../../context/initialContext'; @@ -24,6 +25,13 @@ const LibraryCardStackNavigator = () => { libraryContext: JSON.stringify(React.useContext(LibrarySystemContext)), }} /> + ); }; diff --git a/code/aspen_app/src/screens/GroupedWork/Editions.js b/code/aspen_app/src/screens/GroupedWork/Editions.js index d649256853..fe0fbfe656 100644 --- a/code/aspen_app/src/screens/GroupedWork/Editions.js +++ b/code/aspen_app/src/screens/GroupedWork/Editions.js @@ -17,7 +17,7 @@ import { stripHTML } from '../../util/apiAuth'; import { placeHold } from '../../util/recordActions'; import { getStatusIndicator } from './StatusIndicator'; import { ActionButton } from '../../components/Action/ActionButton'; -import { LanguageContext, LibrarySystemContext, ThemeContext } from '../../context/initialContext'; +import { LanguageContext, LibrarySystemContext, ThemeContext, UserContext } from '../../context/initialContext'; import { getTermFromDictionary } from '../../translations/TranslationService'; export const Editions = () => { @@ -28,6 +28,7 @@ export const Editions = () => { const params = route[0].params; const { id, recordId, format, source, volumeInfo, prevRoute } = params; const { library } = React.useContext(LibrarySystemContext); + const { user } = React.useContext(UserContext); const { language } = React.useContext(LanguageContext); const { colorMode, theme, textColor } = React.useContext(ThemeContext); const [isLoading, setLoading] = React.useState(false); @@ -54,6 +55,39 @@ export const Editions = () => { const [holdSelectItemResponse, setHoldSelectItemResponse] = React.useState(''); const [placingItemHold, setPlacingItemHold] = React.useState(false); + let shouldPromptAlternateLibraryCard = false; + let shouldShowAlternateLibraryCard = false; + let useAlternateCardForCloudLibrary = false; + let userHasAlternateLibraryCard = false; + + if (typeof library.showAlternateLibraryCard !== 'undefined') { + if (library.showAlternateLibraryCard === '1' || library.showAlternateLibraryCard === 1) { + shouldShowAlternateLibraryCard = true; + } + } + + if (typeof library.useAlternateCardForCloudLibrary !== 'undefined') { + if (library.useAlternateCardForCloudLibrary === '1' || library.useAlternateCardForCloudLibrary === 1) { + useAlternateCardForCloudLibrary = true; + } + } + + if (shouldShowAlternateLibraryCard && useAlternateCardForCloudLibrary && source === 'cloud_library') { + shouldPromptAlternateLibraryCard = true; + } + + if (typeof user.alternateLibraryCard !== 'undefined') { + if (user.alternateLibraryCard && user.alternateLibraryCard !== '') { + if (library.alternateLibraryCardConfig?.showAlternateLibraryCardPassword === '1') { + if (user.alternateLibraryCardPassword !== '') { + userHasAlternateLibraryCard = true; + } + } else { + userHasAlternateLibraryCard = true; + } + } + } + const handleNavigation = (action) => { if (prevRoute === 'DiscoveryScreen' || prevRoute === 'SearchResults' || prevRoute === 'HomeScreen') { if (action.includes('Checkouts')) { @@ -117,6 +151,8 @@ export const Editions = () => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> @@ -256,7 +292,7 @@ export const Editions = () => { const Edition = (payload) => { const { language } = React.useContext(LanguageContext); const { theme, textColor } = React.useContext(ThemeContext); - const { response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, holdSelectItemResponse, setHoldSelectItemResponse, holdItemSelectIsOpen, setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef } = payload; + const { response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, holdSelectItemResponse, setHoldSelectItemResponse, holdItemSelectIsOpen, setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, userHasAlternateLibraryCard, shouldPromptAlternateLibraryCard } = payload; const prevRoute = payload.prevRoute; const records = payload.records; const id = payload.id; @@ -343,6 +379,8 @@ const Edition = (payload) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> diff --git a/code/aspen_app/src/screens/GroupedWork/Variations.js b/code/aspen_app/src/screens/GroupedWork/Variations.js index 844583954c..0395889176 100644 --- a/code/aspen_app/src/screens/GroupedWork/Variations.js +++ b/code/aspen_app/src/screens/GroupedWork/Variations.js @@ -257,6 +257,7 @@ export const Variations = (props) => { }; const Variation = (payload) => { + const { user } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); const { language } = React.useContext(LanguageContext); const { textColor, colorMode, theme } = React.useContext(ThemeContext); @@ -273,6 +274,39 @@ const Variation = (payload) => { const isbn = variation.isbn ?? null; const oclcNumber = variation.oclcNumber ?? null; + let shouldPromptAlternateLibraryCard = false; + let shouldShowAlternateLibraryCard = false; + let useAlternateCardForCloudLibrary = false; + let userHasAlternateLibraryCard = false; + + if (typeof library.showAlternateLibraryCard !== 'undefined') { + if (library.showAlternateLibraryCard === '1' || library.showAlternateLibraryCard === 1) { + shouldShowAlternateLibraryCard = true; + } + } + + if (typeof library.useAlternateCardForCloudLibrary !== 'undefined') { + if (library.useAlternateCardForCloudLibrary === '1' || library.useAlternateCardForCloudLibrary === 1) { + useAlternateCardForCloudLibrary = true; + } + } + + if (shouldShowAlternateLibraryCard && useAlternateCardForCloudLibrary && source === 'cloud_library') { + shouldPromptAlternateLibraryCard = true; + } + + if (typeof user.alternateLibraryCard !== 'undefined') { + if (user.alternateLibraryCard && user.alternateLibraryCard !== '') { + if (library.alternateLibraryCardConfig?.showAlternateLibraryCardPassword === '1') { + if (user.alternateLibraryCardPassword !== '') { + userHasAlternateLibraryCard = true; + } + } else { + userHasAlternateLibraryCard = true; + } + } + } + let fullRecordId = _.split(variation.id, ':'); const recordId = _.toString(fullRecordId[1]); @@ -357,6 +391,8 @@ const Variation = (payload) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> diff --git a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js new file mode 100644 index 0000000000..95880aebe2 --- /dev/null +++ b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js @@ -0,0 +1,186 @@ +import _ from 'lodash'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { Pressable, ChevronLeftIcon, Box, ScrollView, ButtonGroup, Button, ButtonText, FormControl, FormControlLabel, FormControlLabelText, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; +import React from 'react'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRoute, useNavigation, CommonActions, StackActions } from '@react-navigation/native'; +import { LoadingSpinner } from '../../../components/loadingSpinner'; + +// custom components and helper files +import { LanguageContext, LibrarySystemContext, SystemMessagesContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { DisplaySystemMessage } from '../../../components/Notifications'; +import { BackIcon } from '../../../themes/theme'; +import { getTermFromDictionary } from '../../../translations/TranslationService'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; +import { decodeHTML } from '../../../util/apiAuth'; + +export const MyAlternateLibraryCard = () => { + const navigation = useNavigation(); + const route = useRoute(); + const { library } = React.useContext(LibrarySystemContext); + const { user, updateUser } = React.useContext(UserContext); + const { language } = React.useContext(LanguageContext); + const { theme, textColor, colorMode } = React.useContext(ThemeContext); + const queryClient = useQueryClient(); + const { systemMessages, updateSystemMessages } = React.useContext(SystemMessagesContext); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + + const [isLoading, setIsLoading] = React.useState(false); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const handleGoBack = () => { + console.log(route?.params); + if (route?.params?.prevRoute === 'AccountDrawer') { + navigation.dispatch(CommonActions.setParams({ prevRoute: null })); + navigation.dispatch(StackActions.replace('LibraryCard')); + } else { + navigation.goBack(); + } + }; + + React.useLayoutEffect(() => { + navigation.setOptions({ + headerLeft: () => ( + + + + ), + }); + }, [navigation]); + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + let alternateLibraryCardStyle = 'none'; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardStyle) { + alternateLibraryCardStyle = library.alternateLibraryCardConfig.alternateLibraryCardStyle; + } + + const showSystemMessage = () => { + if (_.isArray(systemMessages)) { + return systemMessages.map((obj, index, collection) => { + if (obj.showOn === '0' || obj.showOn === '1' || obj.showOn === '5') { + return ; + } + }); + } + return null; + }; + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const deleteCard = async () => { + await updateAlternateLibraryCard('', '', true, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + + return ( + + {isLoading ? ( + LoadingSpinner() + ) : ( + + {showSystemMessage()} + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + )} + + ); +}; \ No newline at end of file diff --git a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js index 6a2e315a75..9927c0bc8f 100644 --- a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js +++ b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js @@ -15,8 +15,10 @@ import Carousel from 'react-native-reanimated-carousel'; // custom components and helper files import { PermissionsPrompt } from '../../../components/PermissionsPrompt'; import { LanguageContext, LibrarySystemContext, UserContext } from '../../../context/initialContext'; +import { navigateStack } from '../../../helpers/RootNavigator'; import { getTermFromDictionary, getTranslationsWithValues } from '../../../translations/TranslationService'; import { getLinkedAccounts, updateScreenBrightnessStatus } from '../../../util/api/user'; +import { formatDiscoveryVersion } from '../../../util/loadLibrary'; export const MyLibraryCard = () => { const queryClient = useQueryClient(); @@ -165,6 +167,15 @@ export const MyLibraryCard = () => { return ; } + const version = formatDiscoveryVersion(library.discoveryVersion); + let shouldShowAlternateLibraryCard = false; + if (typeof library.showAlternateLibraryCard !== 'undefined') { + shouldShowAlternateLibraryCard = library.showAlternateLibraryCard; + } + if (version >= '24.09.00' && (shouldShowAlternateLibraryCard === '1' || shouldShowAlternateLibraryCard === 1)) { + shouldShowAlternateLibraryCard = true; + } + /* useFocusEffect( React.useCallback(() => { console.log("numCards listener > " + numCards); @@ -193,6 +204,21 @@ export const MyLibraryCard = () => { return ( <> + {shouldShowAlternateLibraryCard ? ( +
+ +
+ ) : null} ); }; diff --git a/code/aspen_app/src/translations/defaults.json b/code/aspen_app/src/translations/defaults.json index 53ad04bfd7..62a4ff3d57 100644 --- a/code/aspen_app/src/translations/defaults.json +++ b/code/aspen_app/src/translations/defaults.json @@ -570,5 +570,8 @@ "mark_as_unread": "Mark As Unread", "date_sent": "Date Sent", "sent_on": "Sent on %1%", - "open": "Open" + "open": "Open", + "alternate_library_card": "Alternate Library Card", + "manage_alternate_library_card": "Manage Alternate Library Card", + "add_alternate_library_card": "Add Alternate Library Card" } \ No newline at end of file diff --git a/code/aspen_app/src/util/api/user.js b/code/aspen_app/src/util/api/user.js index 47774941f5..099ca3285c 100644 --- a/code/aspen_app/src/util/api/user.js +++ b/code/aspen_app/src/util/api/user.js @@ -188,6 +188,42 @@ export async function logoutUser(url) { } } +/** + * Updates the users alternate library card + * @param {string} cardNumber + * @param {string} cardPassword + * @param {boolean} deleteCard + * @param {string} url + * @param {string} language + **/ +export async function updateAlternateLibraryCard(cardNumber = '', cardPassword = '', deleteCard = false, url, language = 'en') { + const postBody = await postData(); + postBody.append('alternateLibraryCard', cardNumber); + postBody.append('alternateLibraryCardPassword', cardPassword); + + const api = create({ + baseURL: url + '/API', + headers: getHeaders(true), + auth: createAuthTokens(), + params: { + deleteAlternateLibraryCard: deleteCard, + language, + }, + }); + + const response = await api.post('/UserAPI?method=updateAlternateLibraryCard', postBody); + let data = []; + if (response.ok) { + data = response.data; + } + + return { + success: data?.success ?? false, + title: data?.title ?? null, + message: data?.message ?? null, + }; +} + /** ******************************************************************* * Checkouts and Holds ******************************************************************* **/ diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index 5d3346a046..d218d0b316 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -1,4 +1,6 @@ ## Aspen LiDA Updates +- Added prompts for providing an alternate library card while placing a hold or checking out cloudLibrary items. (*KK*) +- Added a screen to modify or remove alternative library card information, if enabled for the library. This screen is accessible in both the Account Drawer and on the Card screens. (*KK*) ## Aspen Discovery Updates // mark - ByWater From 8135b645c551932244e21af1232efca857636d3d Mon Sep 17 00:00:00 2001 From: Lucas Montoya Date: Thu, 22 Aug 2024 10:40:02 -0300 Subject: [PATCH 06/56] Move cron process to foreground - Execute checkBackgroundProcesses in background - Execute cron process in foreground to keep the container alive - Remove 'sleep infinity' trick in case cron is enabled --- docker/files/scripts/start.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/files/scripts/start.sh b/docker/files/scripts/start.sh index c04b02d588..86d307d2ed 100755 --- a/docker/files/scripts/start.sh +++ b/docker/files/scripts/start.sh @@ -62,9 +62,9 @@ curl -k http://"$SITE_NAME"/API/SystemAPI?method=runPendingDatabaseUpdates # Start Cron if [ "$ASPEN_CRON" == "yes" ]; then - service cron start php /usr/local/aspen-discovery/code/web/cron/checkBackgroundProcesses.php "$SITE_NAME" & + cron -f -L 2 +else +/bin/bash -c "trap : TERM INT; sleep infinity & wait" fi -# Infinite loop -/bin/bash -c "trap : TERM INT; sleep infinity & wait" From 1701d5d7496f76ac85a17156ef00e05bea460640 Mon Sep 17 00:00:00 2001 From: Lucas Montoya Date: Thu, 22 Aug 2024 10:46:44 -0300 Subject: [PATCH 07/56] Remove entrypoint in cron service --- code/web/release_notes/24.09.00.MD | 1 + docker/docker-compose.yml | 1 - docker/files/scripts/start.sh | 22 +++++++++++----------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index 70664c0fb6..88947c43c2 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -124,6 +124,7 @@ // lucas ### Other updates - Add supportingCompany support in Docker scripts. (*LM*) +- Now cron process is executed in foreground at the end of the start up script. (*LM*) // James Staub ### Reports diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index aab14a4e7e..6ddcb18056 100755 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -38,7 +38,6 @@ services: - ${ASPEN_DATA_DIR}/logs:/var/log/aspen-discovery depends_on: - backend - entrypoint: cron -f -L 2 db: image: ${MARIADB_IMAGE:-mariadb:10.5} diff --git a/docker/files/scripts/start.sh b/docker/files/scripts/start.sh index 86d307d2ed..4dc025199d 100755 --- a/docker/files/scripts/start.sh +++ b/docker/files/scripts/start.sh @@ -9,30 +9,30 @@ cd "/usr/local/aspen-discovery/docker/files/scripts" || exit # Check if site configuration exists confSiteFile="$CONFIG_DIRECTORY/conf/config.ini" if [ ! -f "$confSiteFile" ] ; then - mkdir -p "$CONFIG_DIRECTORY" - if ! php createConfig.php "$CONFIG_DIRECTORY" ; then - echo "ERROR : FAILED TO CREATE ASPEN SETTINGS" - exit 1 - fi + mkdir -p "$CONFIG_DIRECTORY" + if ! php createConfig.php "$CONFIG_DIRECTORY" ; then + echo "ERROR : FAILED TO CREATE ASPEN SETTINGS" + exit 1 + fi fi # Initialize Aspen database if ! php initDatabase.php ; then - echo "ERROR : FAILED TO INITIALIZE DATABASE" - exit 1 + echo "ERROR : FAILED TO INITIALIZE DATABASE" + exit 1 fi # Initialize Koha Connection if ! php initKohaLink.php ; then - echo "ERROR : FAILED TO ESTABLISH A CONNECTION WITH KOHA" - exit 1 + echo "ERROR : FAILED TO ESTABLISH A CONNECTION WITH KOHA" + exit 1 fi # Create missing dirs and fix ownership and permissions if needed if ! php createDirs.php ; then - echo "ERROR : FAILED TO CREATE DIRECTORIES OR TRY TO FIX OWNERSHIP AND PERMISSIONS" - exit 1 + echo "ERROR : FAILED TO CREATE DIRECTORIES OR TRY TO FIX OWNERSHIP AND PERMISSIONS" + exit 1 fi # Move and create temporarily sym-links to etc/cron directory From ab4d637916e36964da8e3be4882cbef84ccfffd2 Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Fri, 23 Aug 2024 09:27:43 -0500 Subject: [PATCH 08/56] Update version.json --- code/aspen_app/app-configs/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/aspen_app/app-configs/version.json b/code/aspen_app/app-configs/version.json index e64102c80a..7e30530f69 100644 --- a/code/aspen_app/app-configs/version.json +++ b/code/aspen_app/app-configs/version.json @@ -1,5 +1,5 @@ { - "version": "24.08.00", - "build": "270", + "version": "24.09.00", + "build": "275", "patch": "0" } \ No newline at end of file From 779608da46bc156b03a64ea4799acc961507a57a Mon Sep 17 00:00:00 2001 From: James Staub Date: Fri, 23 Aug 2024 10:05:58 -0500 Subject: [PATCH 09/56] Fixes Nashville holds report Fixes mixup on item-level holds between patron name and pickup branch. https://trello.com/c/RQQkLQ3L/3844-add-robertson-academy-gate#comment-66c656a7e4aae5792da48f87 --- code/web/Drivers/Nashville.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/web/Drivers/Nashville.php b/code/web/Drivers/Nashville.php index 755f190009..b3d6b734fe 100644 --- a/code/web/Drivers/Nashville.php +++ b/code/web/Drivers/Nashville.php @@ -524,8 +524,8 @@ public function getHoldsReportData($location): array { ), item_level_holds as ( select - pb.branchname as PICKUP_BRANCH - , p.name as PATRON_NAME + p.name as PATRON_NAME + , pb.branchname as PICKUP_BRANCH , p.sponsor as HOME_ROOM , bb.btyname as GRD_LVL , p.patronid as P_BARCODE From cf59b92c34e382a41ad1de88d9a1eca4f593b8bc Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Fri, 23 Aug 2024 12:25:11 -0500 Subject: [PATCH 10/56] Fix for LiDA hold freeze translations - When freezing a hold, fixed a bug where the date picker actions when selecting the thaw date were not using translated values. --- .../screens/MyAccount/TitlesOnHold/MyHold.js | 21 ++++++++++++------- .../MyAccount/TitlesOnHold/SelectThawDate.js | 8 ++++--- code/web/release_notes/24.09.00.MD | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js b/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js index 17e61a8e20..c5cccb0279 100644 --- a/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js +++ b/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js @@ -1,16 +1,16 @@ import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; -import CachedImage from 'expo-cached-image'; +import DateTimePickerModal from 'react-native-modal-datetime-picker'; import { Image } from 'expo-image'; import _ from 'lodash'; -import { Actionsheet, Box, Button, Center, Checkbox, HStack, Icon, Pressable, Text, useDisclose, VStack } from 'native-base'; +import { Actionsheet, Box, Button, Center, Checkbox, HStack, Icon, Pressable, Text, useDisclose, VStack, useToken, useColorModeValue } from 'native-base'; import React from 'react'; import { popAlert } from '../../../components/loadError'; import { HoldsContext, LanguageContext, LibrarySystemContext, UserContext } from '../../../context/initialContext'; import { getAuthor, getBadge, getCleanTitle, getExpirationDate, getFormat, getOnHoldFor, getPickupLocation, getPosition, getStatus, getTitle, getType } from '../../../helpers/item'; import { navigateStack } from '../../../helpers/RootNavigator'; -import { getTermFromDictionary, getTranslationsWithValues } from '../../../translations/TranslationService'; -import { cancelHold, cancelHolds, cancelVdxRequest, thawHold, thawHolds } from '../../../util/accountActions'; +import { getTermFromDictionary } from '../../../translations/TranslationService'; +import { cancelHold, cancelHolds, cancelVdxRequest, freezeHold, freezeHolds, thawHold, thawHolds } from '../../../util/accountActions'; import { formatDiscoveryVersion } from '../../../util/loadLibrary'; import { checkoutItem } from '../../../util/recordActions'; import { SelectPickupLocation } from './SelectPickupLocation'; @@ -89,6 +89,9 @@ export const MyHold = (props) => { } } + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); + const openGroupedWork = (item, title) => { navigateStack('AccountScreenTab', 'MyHold', { id: item, @@ -251,7 +254,7 @@ export const MyHold = (props) => { ); } else { - return ; + return ; } } else { return null; @@ -371,6 +374,8 @@ export const ManageSelectedHolds = (props) => { const numToFreezeLabel = getTermFromDictionary(language, 'freeze_selected_holds') + ' (' + numToFreeze + ')'; const numToThawLabel = getTermFromDictionary(language, 'thaw_selected_holds') + ' (' + numToThaw + ')'; const numSelectedLabel = getTermFromDictionary(language, 'manage_selected') + ' (' + numSelected + ')'; + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); const cancelActionItem = () => { if (numToCancel > 0) { @@ -424,7 +429,7 @@ export const ManageSelectedHolds = (props) => { {cancelActionItem()} - + {thawActionItem()} @@ -489,6 +494,8 @@ export const ManageAllHolds = (props) => { const numToCancelLabel = getTermFromDictionary(language, 'cancel_all_holds') + ' (' + numToCancel + ')'; const numToFreezeLabel = getTermFromDictionary(language, 'freeze_all_holds') + ' (' + numToFreeze + ')'; const numToThawLabel = getTermFromDictionary(language, 'thaw_all_holds') + ' (' + numToThaw + ')'; + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); if (numToManage >= 1) { return ( @@ -511,7 +518,7 @@ export const ManageAllHolds = (props) => { }}> {numToCancelLabel} - + { - const { label, language, libraryContext, onClose, freezeId, recordId, source, userId, resetGroup, isOpen } = props; + const { freezingLabel, freezeLabel, label, libraryContext, onClose, freezeId, recordId, source, userId, resetGroup, isOpen } = props; let data = props.data; + const { language } = React.useContext(LanguageContext); const [loading, setLoading] = React.useState(false); const textColor = useToken('colors', useColorModeValue('text.500', 'text.50')); const colorMode = useColorModeValue(false, true); - let actionLabel = getTermFromDictionary(language, 'freeze_hold'); + let actionLabel = freezeLabel; if (label) { actionLabel = label; } @@ -57,7 +59,7 @@ export const SelectThawDate = (props) => { } onPress={showDatePicker}> {actionLabel} - + ); }; \ No newline at end of file diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index 5baa170aea..b0efdb26cb 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -1,6 +1,7 @@ ## Aspen LiDA Updates - Added prompts for providing an alternate library card while placing a hold or checking out cloudLibrary items. (*KK*) - Added a screen to modify or remove alternative library card information, if enabled for the library. This screen is accessible in both the Account Drawer and on the Card screens. (*KK*) +- When freezing a hold, fixed a bug where the date picker actions when selecting the thaw date were not using translated values. (*KK*) ## Aspen Discovery Updates // mark - ByWater From 3d06c0f794d072dba8f68cf6944b784c2d5c55e4 Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Fri, 23 Aug 2024 12:25:55 -0500 Subject: [PATCH 11/56] Update version.json --- code/aspen_app/app-configs/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/aspen_app/app-configs/version.json b/code/aspen_app/app-configs/version.json index 7e30530f69..e8af23c3e9 100644 --- a/code/aspen_app/app-configs/version.json +++ b/code/aspen_app/app-configs/version.json @@ -1,5 +1,5 @@ { "version": "24.09.00", "build": "275", - "patch": "0" + "patch": "1" } \ No newline at end of file From 82bfd41f1345f7f4dc46f8df2c6fc55e63a4132c Mon Sep 17 00:00:00 2001 From: Kirstien Kroeger Date: Fri, 23 Aug 2024 15:20:08 -0500 Subject: [PATCH 12/56] LiDA Radio Facet Fix Fixed a bug where when selecting radio button values in Facets (i.e. Search Within), it would display a different (previously selected) value as being selected. --- code/aspen_app/app-configs/version.json | 2 +- .../aspen_app/src/screens/Search/Facets/RadioGroup.js | 11 ++++++++++- code/web/release_notes/24.09.00.MD | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/code/aspen_app/app-configs/version.json b/code/aspen_app/app-configs/version.json index e8af23c3e9..8556ad677b 100644 --- a/code/aspen_app/app-configs/version.json +++ b/code/aspen_app/app-configs/version.json @@ -1,5 +1,5 @@ { "version": "24.09.00", "build": "275", - "patch": "1" + "patch": "2" } \ No newline at end of file diff --git a/code/aspen_app/src/screens/Search/Facets/RadioGroup.js b/code/aspen_app/src/screens/Search/Facets/RadioGroup.js index 3be58f3f7e..3f0a399060 100644 --- a/code/aspen_app/src/screens/Search/Facets/RadioGroup.js +++ b/code/aspen_app/src/screens/Search/Facets/RadioGroup.js @@ -41,7 +41,9 @@ export default class Facet_RadioGroup extends Component { componentDidUpdate(prevProps, prevState) { if (prevState.value !== this.state.applied) { - this.renderValue(); + console.log('prevState.value', prevState.value); + console.log('this.state.applied', this.state.applied); + //this.renderValue(); } } @@ -59,17 +61,22 @@ export default class Facet_RadioGroup extends Component { const { category, value } = this.state; if (category !== 'sort_by') { console.log('payload > ', payload); + console.log('value > ', value); if (payload === value) { + console.log('new is same as old. removing.'); removeAppliedFilter(category, payload); this.setState({ value: '', }); } else { + console.log('new value. adding.'); addAppliedFilter(category, payload, false); this.setState({ value: payload, }); } + + console.log('current state value: ' + this.state.value); } else { console.log('payload > ', payload); console.log('value > ', value); @@ -94,6 +101,8 @@ export default class Facet_RadioGroup extends Component { const { items, category, title, updater, applied } = this.state; const name = category + '_group'; + console.log(items); + if (category === 'sort_by') { return ( diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index b0efdb26cb..91fed4e126 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -1,4 +1,5 @@ ## Aspen LiDA Updates +- Fixed a bug where when selecting radio button values in Facets (i.e. Search Within), it would display a different value as being selected. (Ticket 132013) (*KK*) - Added prompts for providing an alternate library card while placing a hold or checking out cloudLibrary items. (*KK*) - Added a screen to modify or remove alternative library card information, if enabled for the library. This screen is accessible in both the Account Drawer and on the Card screens. (*KK*) - When freezing a hold, fixed a bug where the date picker actions when selecting the thaw date were not using translated values. (*KK*) From 2a0f2a4c41df1009e4adea523b05c6a96d64fe82 Mon Sep 17 00:00:00 2001 From: James Staub Date: Fri, 23 Aug 2024 20:50:47 -0500 Subject: [PATCH 13/56] Creates barcode generator report https://trello.com/c/PNcsJLs7 Provided a list of numbers, print Code 39 barcodes for each number on Avery 5160 labels --- code/web/.idea/dataSources.local.xml | 2 +- .../responsive/Report/barcodeGenerator.tpl | 130 ++++++++++++++++++ code/web/services/Report/BarcodeGenerator.php | 28 ++++ code/web/sys/Account/User.php | 14 +- 4 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 code/web/interface/themes/responsive/Report/barcodeGenerator.tpl create mode 100644 code/web/services/Report/BarcodeGenerator.php diff --git a/code/web/.idea/dataSources.local.xml b/code/web/.idea/dataSources.local.xml index 171413b11b..5a58003517 100644 --- a/code/web/.idea/dataSources.local.xml +++ b/code/web/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + #@ diff --git a/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl new file mode 100644 index 0000000000..de11193589 --- /dev/null +++ b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl @@ -0,0 +1,130 @@ + +{strip} + + + +
+
+ +
+

MNPS/NPL patron barcode generator

+

Before printing labels, use your browser’s print preview options to

+
    +
  • set all header and footer fields to BLANK
  • +
  • set all print margins to 0 (zero)
  • +
  • set Scale to 100%; do NOT shrink to fit
  • +
+

Try printing a test page on plain paper first. Hold it up to the light behind a sheet of labels to make sure the bar codes line up with the stickers.

+ +
+ Patron ID:
+ +
+ +
+
+ +{/strip} \ No newline at end of file diff --git a/code/web/services/Report/BarcodeGenerator.php b/code/web/services/Report/BarcodeGenerator.php new file mode 100644 index 0000000000..c56f84653a --- /dev/null +++ b/code/web/services/Report/BarcodeGenerator.php @@ -0,0 +1,28 @@ +display('barcodeGenerator.tpl', 'Barcode Generator', ''); + } + + function getBreadcrumbs(): array { + $breadcrumbs = []; + $breadcrumbs[] = new Breadcrumb('/Admin/Home', 'Administration Home'); + $breadcrumbs[] = new Breadcrumb('/Admin/Home#circulation_reports', 'Circulation Reports'); + $breadcrumbs[] = new Breadcrumb('', 'Barcode Generator'); + return $breadcrumbs; + } + + function getActiveAdminSection(): string { + return 'circulation_reports'; + } + function canView(): bool { + return UserAccount::userHasPermission([ + 'View All Student Reports', + 'View Location Student Reports', + ]); + } +} \ No newline at end of file diff --git a/code/web/sys/Account/User.php b/code/web/sys/Account/User.php index efdaa7120f..d157848e65 100644 --- a/code/web/sys/Account/User.php +++ b/code/web/sys/Account/User.php @@ -3605,7 +3605,15 @@ public function getAdminActions() { if ($circulationReports) { $sections['circulation_reports'] = new AdminSection('Circulation Reports'); - $sections['circulation_reports']->addAction(new AdminAction('Holds Report', 'View a report of holds to be pulled from the shelf for patrons.', '/Report/HoldsReport'), [ + $sections['circulation_reports']->addAction(new AdminAction('Barcode Generator', 'Create Code39 barcodes on Avery 5160 labels from a list of numbers.', '/Report/BarcodeGenerator'), [ + 'View Location Student Reports', + 'View All Student Reports', + ]); + $sections['circulation_reports']->addAction(new AdminAction('Collection Report', 'View a report of all items for a branch.', '/Report/CollectionReport'), [ + 'View Location Collection Reports', + 'View All Collection Reports', + ]); + $sections['circulation_reports']->addAction(new AdminAction('Holds Report', 'View a report of holds to be pulled from the shelf for patrons.', '/Report/HoldsReport'), [ 'View Location Holds Reports', 'View All Holds Reports', ]); @@ -3617,10 +3625,6 @@ public function getAdminActions() { 'View Location Student Reports', 'View All Student Reports', ]); - $sections['circulation_reports']->addAction(new AdminAction('Collection Report', 'View a report of all items for a branch.', '/Report/CollectionReport'), [ - 'View Location Collection Reports', - 'View All Collection Reports', - ]); $sections['circulation_reports']->addAction(new AdminAction('Weeding Report', 'View a collection weeding report for all items for a branch.', '/Report/WeedingReport'), [ 'View Location Collection Reports', 'View All Collection Reports', From ab7196e807312824b67ff2bdef92533f42d7e955 Mon Sep 17 00:00:00 2001 From: James Staub Date: Sat, 24 Aug 2024 09:10:25 -0500 Subject: [PATCH 14/56] Improves Barcode Generator + Enables sidebar Admin Navigation + Replaces "patron id" language with "barcode numbers" --- .../responsive/Report/barcodeGenerator.tpl | 68 +++++++++---------- code/web/services/Report/BarcodeGenerator.php | 3 +- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl index de11193589..2f3f9da031 100644 --- a/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl +++ b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl @@ -1,25 +1,25 @@ - -{strip} + 20170817 v.1 Avery 5160 + --> + + + + +
+

Disc Barcode Generator

+

Print disc hub circular EAN-8 barcodes for each number supplied

+
+ Numbers to print as barcodes (each number on a new line):
+ + + +   +   +
+
+
+
+{/strip} \ No newline at end of file diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index 70de9b54f2..99d0cd9073 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -131,6 +131,7 @@ // James Staub ### Reports - Adds Code 39 barcode generator for Avery 5160 labels to Circulation Reports. (*JStaub*) +- Adds EAN-8 disc hub barcode generator to Circulation Reports. (*JStaub*) - Improves Nashville-specific Student Barcode report. (*JStaub*) // Jeremy Eden diff --git a/code/web/services/Report/DiscBarcodeGenerator.php b/code/web/services/Report/DiscBarcodeGenerator.php new file mode 100644 index 0000000000..d5d1c9b909 --- /dev/null +++ b/code/web/services/Report/DiscBarcodeGenerator.php @@ -0,0 +1,27 @@ +display('discBarcodeGenerator.tpl', 'Disc Barcode Generator'); + } + + function getBreadcrumbs(): array { + $breadcrumbs = []; + $breadcrumbs[] = new Breadcrumb('/Admin/Home', 'Administration Home'); + $breadcrumbs[] = new Breadcrumb('/Admin/Home#circulation_reports', 'Circulation Reports'); + $breadcrumbs[] = new Breadcrumb('', 'Disc Barcode Generator'); + return $breadcrumbs; + } + + function getActiveAdminSection(): string { + return 'circulation_reports'; + } + function canView(): bool { + return UserAccount::userHasPermission([ + 'View All Collection Reports', + 'View Location Collection Reports', + ]); + } +} \ No newline at end of file diff --git a/code/web/sys/Account/User.php b/code/web/sys/Account/User.php index d157848e65..ad8aa35ee3 100644 --- a/code/web/sys/Account/User.php +++ b/code/web/sys/Account/User.php @@ -3609,6 +3609,10 @@ public function getAdminActions() { 'View Location Student Reports', 'View All Student Reports', ]); + $sections['circulation_reports']->addAction(new AdminAction('Barcode Generator - Disc', 'Create hub EAN-8 barcodes for CDs and DVDs.', '/Report/DiscBarcodeGenerator'), [ + 'View Location Collection Reports', + 'View All Collection Reports', + ]); $sections['circulation_reports']->addAction(new AdminAction('Collection Report', 'View a report of all items for a branch.', '/Report/CollectionReport'), [ 'View Location Collection Reports', 'View All Collection Reports', From 35d8b8d9cf4e0e3a1bac29d42c8931e85d877a76 Mon Sep 17 00:00:00 2001 From: James Staub Date: Sun, 25 Aug 2024 09:19:45 -0500 Subject: [PATCH 19/56] Creates Barcode Generator permissions --- code/web/services/Report/BarcodeGenerator.php | 3 +-- code/web/services/Report/DiscBarcodeGenerator.php | 3 +-- .../sys/DBMaintenance/version_updates/24.09.00.php | 12 +++++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/code/web/services/Report/BarcodeGenerator.php b/code/web/services/Report/BarcodeGenerator.php index c21f570bed..4dad27dfdf 100644 --- a/code/web/services/Report/BarcodeGenerator.php +++ b/code/web/services/Report/BarcodeGenerator.php @@ -20,8 +20,7 @@ function getActiveAdminSection(): string { } function canView(): bool { return UserAccount::userHasPermission([ - 'View All Student Reports', - 'View Location Student Reports', + 'Barcode Generators', ]); } } \ No newline at end of file diff --git a/code/web/services/Report/DiscBarcodeGenerator.php b/code/web/services/Report/DiscBarcodeGenerator.php index d5d1c9b909..6ddb4e982e 100644 --- a/code/web/services/Report/DiscBarcodeGenerator.php +++ b/code/web/services/Report/DiscBarcodeGenerator.php @@ -20,8 +20,7 @@ function getActiveAdminSection(): string { } function canView(): bool { return UserAccount::userHasPermission([ - 'View All Collection Reports', - 'View Location Collection Reports', + 'Barcode Generators', ]); } } \ No newline at end of file diff --git a/code/web/sys/DBMaintenance/version_updates/24.09.00.php b/code/web/sys/DBMaintenance/version_updates/24.09.00.php index 6dad2af123..5206e92dc0 100644 --- a/code/web/sys/DBMaintenance/version_updates/24.09.00.php +++ b/code/web/sys/DBMaintenance/version_updates/24.09.00.php @@ -110,7 +110,17 @@ function getUpdates24_09_00(): array { //pedro - PTFS-Europe //James Staub - Nashville Public Library - + 'barcode_generator_report_permissions' => [ + 'title' => 'Barcode Generator report permissions', + 'description' => 'Create permissions for Barcode Generator reports', + 'continueOnError' => true, + 'sql' => [ + "INSERT INTO permissions (sectionName, name, requiredModule, weight, description) VALUES + ('Circulation Reports', 'Barcode Generators', '', 60, 'Allows the user to run the Barcode Generators') + ", +// "INSERT INTO role_permissions(roleId, permissionId) VALUES ((SELECT roleId from roles where name='locationReports'), (SELECT id from permissions where name='Barcode Generators'))", + ], + ], //other From d3b67a290b5f928a510eac1da2e44a4f08d75089 Mon Sep 17 00:00:00 2001 From: James Staub Date: Sun, 25 Aug 2024 09:20:52 -0500 Subject: [PATCH 20/56] Makes translatable headings for barcode generators --- .../interface/themes/responsive/Report/barcodeGenerator.tpl | 2 +- .../themes/responsive/Report/discBarcodeGenerator.tpl | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl index 0244e20b7f..fca9bd59d9 100644 --- a/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl +++ b/code/web/interface/themes/responsive/Report/barcodeGenerator.tpl @@ -113,7 +113,7 @@ }
-

Barcode Generator

+

{translate text="Barcode Generator" isAdminFacing=true}"

Print barcodes for each number supplied on Avery 5160 labels

Before printing labels, use your browser’s print preview options to

    diff --git a/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl b/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl index aa03967b44..8d10bc3037 100644 --- a/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl +++ b/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl @@ -252,10 +252,9 @@ }
    -

    Disc Barcode Generator

    +

    {translate text="Disc Barcode Generator" isAdminFacing=true}

    Print disc hub circular EAN-8 barcodes for each number supplied

    - Numbers to print as barcodes (each number on a new line):
    From 63a75e61cd3701bbf8c3297f272872e9a120fe74 Mon Sep 17 00:00:00 2001 From: James Staub Date: Sun, 25 Aug 2024 10:20:43 -0500 Subject: [PATCH 21/56] Update 24.09.00.MD --- code/web/release_notes/24.09.00.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/web/release_notes/24.09.00.MD b/code/web/release_notes/24.09.00.MD index 99d0cd9073..3afdbc0581 100644 --- a/code/web/release_notes/24.09.00.MD +++ b/code/web/release_notes/24.09.00.MD @@ -129,6 +129,9 @@ - Now cron process is executed in foreground at the end of the start up script. (*LM*) // James Staub +### New Permissions +- Adds new permission to allow access to the Barcode Generators. (*JStaub*) + ### Reports - Adds Code 39 barcode generator for Avery 5160 labels to Circulation Reports. (*JStaub*) - Adds EAN-8 disc hub barcode generator to Circulation Reports. (*JStaub*) From ffb91ed0edd7aa4b66e154e9ef3c2dccaa92c3d8 Mon Sep 17 00:00:00 2001 From: James Staub Date: Sun, 25 Aug 2024 10:21:28 -0500 Subject: [PATCH 22/56] Adds validation to disc barcode generator --- .../Report/discBarcodeGenerator.tpl | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl b/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl index 8d10bc3037..d79ae113c4 100644 --- a/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl +++ b/code/web/interface/themes/responsive/Report/discBarcodeGenerator.tpl @@ -222,6 +222,28 @@