From 855c2a073d259ce9a9ebf881701bbe38cf84295a Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Fri, 6 Nov 2026 15:27:41 +0100 Subject: [PATCH] Release 1.13.0 (versionCode 27): the visitor log finds a member without the card With Pinakes 0.9.2 (Mobile API 1.11.0) the desk tab offers Find member: the loan desk's borrower search, and a picked member is checked in with user_id under its own Idempotency-Key. Shown only when /health advertises staff_attendance_member_pick. --- STATUS.md | 7 ++ app/build.gradle.kts | 4 +- .../java/com/pinakes/app/data/model/Models.kt | 2 + .../app/data/model/Staff090PatronsModels.kt | 8 +- .../repository/Staff090PatronsRepository.kt | 6 +- .../pinakes/app/data/staff/Staff090Areas.kt | 3 + .../app/data/staff/Staff090PatronsErrors.kt | 1 + .../pinakes/app/data/store/Staff090Flags.kt | 2 + .../Staff090PatronsAttendanceScreen.kt | 74 ++++++++++++++-- .../server090/Staff090PatronsParsingTest.kt | 39 +++++++++ .../server090/patrons/att_checkin_member.json | 24 +++++ .../patrons/att_member_and_card.json | 13 +++ .../patrons/att_member_not_found.json | 8 ++ .../server090/patrons/health_092.json | 87 +++++++++++++++++++ i18n/de.json | 3 + i18n/en.json | 3 + i18n/fr.json | 3 + i18n/it.json | 3 + 18 files changed, 278 insertions(+), 12 deletions(-) create mode 100644 app/src/test/resources/server090/patrons/att_checkin_member.json create mode 100644 app/src/test/resources/server090/patrons/att_member_and_card.json create mode 100644 app/src/test/resources/server090/patrons/att_member_not_found.json create mode 100644 app/src/test/resources/server090/patrons/health_092.json diff --git a/STATUS.md b/STATUS.md index dcfa72c..5482287 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,5 +1,12 @@ # Pinakes Android — Build Status +## 1.13.0 (versionCode 27): the visitor log finds a member without the card + +Pairs with Pinakes 0.9.2 (Mobile API 1.11.0). Works with older servers: without `staff_attendance_member_pick` in `/health` the button is not shown and the desk behaves as in 1.12.0. + +- **Find member** in the visitor log's desk tab: a reader who forgot the card is found by name, e-mail, card or phone with the loan desk's borrower search (`GET /staff/users`, at least two characters) and checked in with `user_id` on `POST /staff/attendance/check-ins`, under its own Idempotency-Key, with the same result card, tone and inactive-account warning as a scanned card. The rows show card and e-mail only: loan eligibility (a debt block, for example) plays no part at the entrance. The camera stops reading while the search is open. `404 member_not_found` has its own wording; a `422` keeps the server's text. +- Shown only when the server advertises it (`/health.features.staff_attendance_member_pick`, Pinakes 0.9.2), and with the visitor log on. + ## 1.12.0 (versionCode 26): loans follow the server calendar, and the same-day handover switch Pairs with Pinakes 0.9.0 and 0.9.1 (Mobile API 1.10.0 as shipped in the stable release). Works with older servers: the new fields are optional and their absence keeps the 1.11.0 behaviour. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e0c1468..09dd4cc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -166,8 +166,8 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" minSdk = 26 targetSdk = 35 - versionCode = 26 - versionName = "1.12.0" + versionCode = 27 + versionName = "1.13.0" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/java/com/pinakes/app/data/model/Models.kt b/app/src/main/java/com/pinakes/app/data/model/Models.kt index 78643c8..8ad599d 100644 --- a/app/src/main/java/com/pinakes/app/data/model/Models.kt +++ b/app/src/main/java/com/pinakes/app/data/model/Models.kt @@ -152,6 +152,8 @@ data class HealthFeatures( @SerialName("staff_catalogue_cards") val staffCatalogueCards: Boolean = false, @SerialName("staff_marc") val staffMarc: Boolean = false, @SerialName("staff_attendance") val staffAttendance: Boolean = false, + // Mobile API 1.11.0 (Pinakes 0.9.2): a check-in may name a member picked from the search. + @SerialName("staff_attendance_member_pick") val staffAttendanceMemberPick: Boolean = false, @SerialName("staff_statistics") val staffStatistics: Boolean = false, @SerialName("staff_user_import") val staffUserImport: Boolean = false, ) diff --git a/app/src/main/java/com/pinakes/app/data/model/Staff090PatronsModels.kt b/app/src/main/java/com/pinakes/app/data/model/Staff090PatronsModels.kt index 9027d40..ea3e4d3 100644 --- a/app/src/main/java/com/pinakes/app/data/model/Staff090PatronsModels.kt +++ b/app/src/main/java/com/pinakes/app/data/model/Staff090PatronsModels.kt @@ -240,7 +240,13 @@ data class AttendanceVisit( ) @Serializable -data class AttendanceCheckInRequest(@SerialName("card_code") val cardCode: String, @SerialName("branch_id") val branchId: Int? = null) +data class AttendanceCheckInRequest( + /** The scanned or typed card; null when [userId] names the member (never both). */ + @SerialName("card_code") val cardCode: String? = null, + /** Mobile API 1.11.0: a member picked from `GET /staff/users`. */ + @SerialName("user_id") val userId: Int? = null, + @SerialName("branch_id") val branchId: Int? = null, +) @Serializable data class AttendanceGuestRequest(val name: String? = null, val reason: String? = null, @SerialName("branch_id") val branchId: Int? = null) diff --git a/app/src/main/java/com/pinakes/app/data/repository/Staff090PatronsRepository.kt b/app/src/main/java/com/pinakes/app/data/repository/Staff090PatronsRepository.kt index d30bf8c..6d52a60 100644 --- a/app/src/main/java/com/pinakes/app/data/repository/Staff090PatronsRepository.kt +++ b/app/src/main/java/com/pinakes/app/data/repository/Staff090PatronsRepository.kt @@ -127,7 +127,11 @@ class Staff090PatronsRepository @Inject constructor( suspend fun attendance(days: Int): ApiResult = session.staffCall { api.attendance(days) } suspend fun checkIn(cardCode: String, branchId: Int?, idempotencyKey: String?): ApiResult = - session.staffCall { api.checkIn(AttendanceCheckInRequest(cardCode.trim(), branchId), idempotencyKey) } + session.staffCall { api.checkIn(AttendanceCheckInRequest(cardCode = cardCode.trim(), branchId = branchId), idempotencyKey) } + + /** A member picked from the borrower search (Mobile API 1.11.0), in place of a card. */ + suspend fun checkInMember(userId: Int, branchId: Int?, idempotencyKey: String?): ApiResult = + session.staffCall { api.checkIn(AttendanceCheckInRequest(userId = userId, branchId = branchId), idempotencyKey) } suspend fun guest(name: String?, reason: String?, branchId: Int?, idempotencyKey: String?): ApiResult = session.staffCall { api.guest(AttendanceGuestRequest(name.blankToNull()?.take(150), reason.blankToNull()?.take(255), branchId), idempotencyKey) } diff --git a/app/src/main/java/com/pinakes/app/data/staff/Staff090Areas.kt b/app/src/main/java/com/pinakes/app/data/staff/Staff090Areas.kt index 44dce43..11ccf57 100644 --- a/app/src/main/java/com/pinakes/app/data/staff/Staff090Areas.kt +++ b/app/src/main/java/com/pinakes/app/data/staff/Staff090Areas.kt @@ -26,6 +26,8 @@ data class Staff090Areas( val catalogueCards: Boolean = false, val marc: Boolean = false, val attendance: Boolean = false, + /** The visitor log also checks in a member picked by name (Mobile API 1.11.0, Pinakes 0.9.2). */ + val attendanceMemberPick: Boolean = false, val statistics: Boolean = false, val userImport: Boolean = false, ) { @@ -60,6 +62,7 @@ data class Staff090Areas( catalogueCards = f.staffCatalogueCards, marc = f.staffMarc, attendance = f.staffAttendance, + attendanceMemberPick = f.staffAttendance && f.staffAttendanceMemberPick, statistics = f.staffStatistics, userImport = f.staffUserImport, ) diff --git a/app/src/main/java/com/pinakes/app/data/staff/Staff090PatronsErrors.kt b/app/src/main/java/com/pinakes/app/data/staff/Staff090PatronsErrors.kt index 0d291c1..4e20337 100644 --- a/app/src/main/java/com/pinakes/app/data/staff/Staff090PatronsErrors.kt +++ b/app/src/main/java/com/pinakes/app/data/staff/Staff090PatronsErrors.kt @@ -16,6 +16,7 @@ object Staff090PatronsErrors { "card_not_renewable" -> R.string.staff090_patrons_err_card_not_renewable "amount_exceeds_balance" -> R.string.staff090_patrons_err_amount_exceeds_balance "card_not_found" -> R.string.staff090_patrons_err_card_not_found + "member_not_found" -> R.string.staff090_patrons_err_member_not_found "not_started" -> R.string.staff090_patrons_err_not_started "already_started" -> R.string.staff090_patrons_err_already_started "job_cancelled" -> R.string.staff090_patrons_err_job_cancelled diff --git a/app/src/main/java/com/pinakes/app/data/store/Staff090Flags.kt b/app/src/main/java/com/pinakes/app/data/store/Staff090Flags.kt index 5bd2d26..19bf440 100644 --- a/app/src/main/java/com/pinakes/app/data/store/Staff090Flags.kt +++ b/app/src/main/java/com/pinakes/app/data/store/Staff090Flags.kt @@ -37,6 +37,7 @@ data class Staff090Flags( val staffCatalogueCards: Boolean = false, val staffMarc: Boolean = false, val staffAttendance: Boolean = false, + val staffAttendanceMemberPick: Boolean = false, val staffStatistics: Boolean = false, val staffUserImport: Boolean = false, ) { @@ -72,6 +73,7 @@ data class Staff090Flags( staffCatalogueCards = f.staffCatalogueCards, staffMarc = f.staffMarc, staffAttendance = f.staffAttendance && circulation, + staffAttendanceMemberPick = f.staffAttendanceMemberPick && f.staffAttendance && circulation, staffStatistics = f.staffStatistics, staffUserImport = f.staffUserImport, ) diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/patrons/Staff090PatronsAttendanceScreen.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/patrons/Staff090PatronsAttendanceScreen.kt index 84bcbc9..5840354 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/patrons/Staff090PatronsAttendanceScreen.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/patrons/Staff090PatronsAttendanceScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.PersonAdd +import androidx.compose.material.icons.outlined.PersonSearch import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip @@ -64,7 +65,9 @@ import com.pinakes.app.R import com.pinakes.app.data.model.AttendanceOverview import com.pinakes.app.data.model.AttendanceVisit import com.pinakes.app.data.network.ApiResult +import com.pinakes.app.data.model.StaffUser import com.pinakes.app.data.repository.Staff090PatronsRepository +import com.pinakes.app.data.repository.StaffCirculationRepository import com.pinakes.app.data.repository.StaffFilesRepository import com.pinakes.app.data.repository.isForbidden import com.pinakes.app.data.staff.AttendanceRules @@ -87,6 +90,9 @@ import com.pinakes.app.ui.screens.staff.FormSectionTitle import com.pinakes.app.ui.screens.staff.InfoCard import com.pinakes.app.ui.screens.staff.InfoLine import com.pinakes.app.ui.screens.staff.LeaveWhen +import com.pinakes.app.ui.screens.staff.PickItem +import com.pinakes.app.ui.screens.staff.SearchController +import com.pinakes.app.ui.screens.staff.SearchPickerSheet import com.pinakes.app.ui.screens.staff.StaffMessage import com.pinakes.app.ui.screens.staff.StaffSnackbarEffect import com.pinakes.app.ui.screens.staff.resolve @@ -113,7 +119,8 @@ data class VisitResult( val photo: ByteArray? = null, ) -private data class CheckInSubmission(val code: String, val branchId: Int?) +/** One check-in attempt: a card code or a picked member, at a branch. */ +private data class CheckInSubmission(val code: String?, val userId: Int?, val branchId: Int?) private data class GuestSubmission(val name: String, val reason: String, val branchId: Int?) data class AttendanceUiState( @@ -131,6 +138,8 @@ data class AttendanceUiState( val tonePlayed: Int = 0, val manualError: Boolean = false, val guestSheet: Boolean = false, + /** The member search (Mobile API 1.11.0) is open. */ + val memberSheet: Boolean = false, val savingGuest: Boolean = false, val guestError: StaffMessage? = null, val savingSettings: Boolean = false, @@ -143,7 +152,9 @@ data class AttendanceUiState( /** * The visitor log at the entrance: the phone scans the patron card (CODE 128 or QR), the - * server records the visit and answers the kiosk's message; guests are recorded by hand. The + * server records the visit and answers the kiosk's message; guests are recorded by hand. A + * member who forgot the card is found by name with the loan desk's borrower search and checked + * in by id, on servers that advertise it (`staff_attendance_member_pick`). The * statistics tab shows the counters of the web page, the CSV export and, for administrators, * the retention and the purge. */ @@ -151,6 +162,7 @@ data class AttendanceUiState( class Staff090AttendanceViewModel @Inject constructor( private val patrons: Staff090PatronsRepository, private val files: StaffFilesRepository, + private val circulation: StaffCirculationRepository, ) : ViewModel() { private val _state = MutableStateFlow(AttendanceUiState()) val state: StateFlow = _state.asStateFlow() @@ -158,6 +170,14 @@ class Staff090AttendanceViewModel @Inject constructor( private val checkInKeys = SubmissionKeys() private val guestKeys = SubmissionKeys() + /** The member search: the loan desk's borrower search, at least two characters. */ + val members = SearchController(viewModelScope, minLength = 2) { q -> + when (val res = circulation.users(q)) { + is ApiResult.Success -> ApiResult.Success(res.data.map { it.toVisitorItem() }) + is ApiResult.Failure -> res + } + }.also { it.onFailure = { f -> if (f.isForbidden()) _state.update { s -> s.copy(forbidden = true) } } } + init { load() } fun load(days: Int = _state.value.days) { @@ -189,7 +209,7 @@ class Staff090AttendanceViewModel @Inject constructor( fun selectBranch(id: Int?) = _state.update { it.copy(branchId = id) } fun onDecoded(raw: String) { - if (_state.value.checking || _state.value.guestSheet) return + if (_state.value.checking || _state.value.guestSheet || _state.value.memberSheet) return val code = AttendanceRules.normalizeCard(raw).takeIf { it.isNotEmpty() } ?: return val accepted = debouncer.offer(code, SystemClock.elapsedRealtime()) ?: return checkIn(accepted) @@ -209,13 +229,33 @@ class Staff090AttendanceViewModel @Inject constructor( return true } - private fun checkIn(code: String) { + fun openMembers() { + debouncer.pause() + members.clear() + _state.update { it.copy(memberSheet = true) } + } + + fun closeMembers() { + _state.update { it.copy(memberSheet = false) } + members.clear() + debouncer.resume(SystemClock.elapsedRealtime()) + } + + /** A member picked from the search is checked in by id, like a scanned card. */ + fun pickMember(item: PickItem) { + if (_state.value.checking) return + _state.update { it.copy(memberSheet = false) } + members.clear() + checkIn(code = null, userId = item.id) + } + + private fun checkIn(code: String?, userId: Int? = null) { val branch = _state.value.branchId - val key = checkInKeys.keyFor(CheckInSubmission(code, branch)) + val key = checkInKeys.keyFor(CheckInSubmission(code, userId, branch)) debouncer.pause() _state.update { it.copy(checking = true) } viewModelScope.launch { - val res = patrons.checkIn(code, branch, key) + val res = if (userId != null) patrons.checkInMember(userId, branch, key) else patrons.checkIn(code.orEmpty(), branch, key) val result = when (res) { is ApiResult.Success -> { checkInKeys.reset() @@ -364,10 +404,17 @@ fun Staff090AttendanceScreen(onNavigateUp: () -> Unit) { Tab(selected = tab == state.tab, onClick = { vm.selectTab(tab) }, text = { Text(stringResource(tab.label)) }, modifier = Modifier.testTag("tab-${tab.name.lowercase()}")) } } - if (state.tab == AttendanceTab.DESK) DeskTab(state, vm) else StatsTab(state, vm) + if (state.tab == AttendanceTab.DESK) DeskTab(state, vm, memberPick = areas.s090.attendanceMemberPick) else StatsTab(state, vm) } } if (state.guestSheet) GuestSheet(state, vm) + if (state.memberSheet) { + val s by vm.members.state.collectAsStateWithLifecycle() + SearchPickerSheet( + title = stringResource(R.string.staff090_patrons_member_pick_title), placeholder = stringResource(R.string.staff_search_users), state = s, + onQuery = vm.members::onQuery, onPick = vm::pickMember, onDismiss = vm::closeMembers, hint = stringResource(R.string.staff_search_min_chars), + ) + } if (state.confirmPurge) ConfirmDialog( title = stringResource(R.string.staff090_patrons_purge), body = stringResource(R.string.staff090_patrons_purge_body, state.overview?.retention?.days ?: 0), @@ -379,7 +426,7 @@ fun Staff090AttendanceScreen(onNavigateUp: () -> Unit) { @OptIn(ExperimentalLayoutApi::class) @Composable -private fun DeskTab(state: AttendanceUiState, vm: Staff090AttendanceViewModel) { +private fun DeskTab(state: AttendanceUiState, vm: Staff090AttendanceViewModel, memberPick: Boolean) { Column( Modifier.fillMaxSize().imePadding().verticalScroll(rememberScrollState()).padding(horizontal = Spacing.lg, vertical = Spacing.sm), verticalArrangement = Arrangement.spacedBy(Spacing.md), @@ -423,6 +470,10 @@ private fun DeskTab(state: AttendanceUiState, vm: Staff090AttendanceViewModel) { } } state.last?.let { VisitCard(it) } + if (memberPick) SecondaryButton( + stringResource(R.string.staff090_patrons_member_pick), onClick = vm::openMembers, leadingIcon = Icons.Outlined.PersonSearch, + enabled = !state.checking, modifier = Modifier.fillMaxWidth().testTag("member-open"), + ) SecondaryButton( stringResource(R.string.staff090_patrons_guest), onClick = vm::openGuest, leadingIcon = Icons.Outlined.PersonAdd, modifier = Modifier.fillMaxWidth().testTag("guest-open"), @@ -586,3 +637,10 @@ private fun RetentionCard(overview: AttendanceOverview, saving: Boolean, vm: Sta } } } + +/** A search row for the entrance: name, card and e-mail; loan eligibility plays no part here. */ +internal fun StaffUser.toVisitorItem(): PickItem = PickItem( + id = id, + title = name, + subtitle = listOfNotNull(cardNumber?.takeIf { it.isNotBlank() }, email?.takeIf { it.isNotBlank() }).joinToString(" · ").ifBlank { null }, +) diff --git a/app/src/test/java/com/pinakes/app/server090/Staff090PatronsParsingTest.kt b/app/src/test/java/com/pinakes/app/server090/Staff090PatronsParsingTest.kt index 3e5ea6b..15e0eb8 100644 --- a/app/src/test/java/com/pinakes/app/server090/Staff090PatronsParsingTest.kt +++ b/app/src/test/java/com/pinakes/app/server090/Staff090PatronsParsingTest.kt @@ -1,5 +1,6 @@ package com.pinakes.app.server090 +import com.pinakes.app.data.model.AttendanceCheckInRequest import com.pinakes.app.data.model.AttendanceOverview import com.pinakes.app.data.model.AttendancePurgeResult import com.pinakes.app.data.model.AttendanceSettingsResult @@ -31,6 +32,9 @@ import com.pinakes.app.data.repository.isForbidden import com.pinakes.app.R import com.pinakes.app.ui.screens.staff.s090.patrons.toPatronsMessage import com.pinakes.app.data.store.InstanceFeatures +import com.pinakes.app.data.staff.AttendanceRules +import com.pinakes.app.data.staff.VisitOutcome +import com.pinakes.app.ui.screens.staff.s090.patrons.toVisitorItem import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -61,6 +65,41 @@ class Staff090PatronsParsingTest { assertTrue(areas.ledger && areas.attendance && areas.userImport) } + @Test fun aServerThatTakesAPickedMemberSaysSo() { + // Pinakes 0.9.2 (Mobile API 1.11.0): the visitor log also checks in a member by id. + val areas = Staff090Areas.of("staff", InstanceFeatures.from(data("health_092")), online = true) + assertTrue(areas.attendance && areas.attendanceMemberPick) + // A 0.9.0 / 0.9.1 server has no flag: the search stays hidden. + assertFalse(Staff090Areas.of("staff", InstanceFeatures.from(data("health")), online = true).attendanceMemberPick) + } + + @Test fun aPickedMemberIsSentByIdAndRecordedLikeACard() { + val app = Json { ignoreUnknownKeys = true; explicitNulls = false; isLenient = true; coerceInputValues = true } + assertEquals("""{"user_id":6805}""", app.encodeToString(AttendanceCheckInRequest.serializer(), AttendanceCheckInRequest(userId = 6805))) + assertEquals("""{"card_code":"GB2026","branch_id":2}""", app.encodeToString(AttendanceCheckInRequest.serializer(), AttendanceCheckInRequest(cardCode = "GB2026", branchId = 2))) + val visit = data("att_checkin_member") + assertEquals("member", visit.type) + assertEquals(6805, visit.member?.id) + assertEquals(VisitOutcome.WELCOME, AttendanceRules.outcome(visit)) + val missing = failure("att_member_not_found", 404) + assertEquals("member_not_found", missing.code) + assertEquals(R.string.staff090_patrons_err_member_not_found, Staff090PatronsErrors.messageRes(missing)) + assertEquals(VisitOutcome.ERROR, AttendanceRules.outcome(missing)) + // Card and member together: the server's precise 422 text is kept. + val both = failure("att_member_and_card", 422) + assertEquals("validation", both.code) + assertNull(Staff090PatronsErrors.messageRes(both)) + } + + @Test fun theEntranceSearchShowsCardAndMailWithoutLoanEligibility() { + val blocked = data>("users_search").single().copy(eligible = false, eligibilityReason = "debt_block") + val item = blocked.toVisitorItem() + assertEquals(blocked.id, item.id) + assertTrue(item.enabled) + assertNull(item.note) + assertEquals(listOfNotNull(blocked.cardNumber, blocked.email).filter { it.isNotBlank() }.joinToString(" · ").ifBlank { null }, item.subtitle) + } + @Test fun staffUserCarriesTheNewPatronFields() { val user = data("user_detail") assertEquals("PCAP090A", user.cardNumber) diff --git a/app/src/test/resources/server090/patrons/att_checkin_member.json b/app/src/test/resources/server090/patrons/att_checkin_member.json new file mode 100644 index 0000000..85919f7 --- /dev/null +++ b/app/src/test/resources/server090/patrons/att_checkin_member.json @@ -0,0 +1,24 @@ +{ + "data": { + "id": 60, + "type": "member", + "created_at": "2026-11-06 15:25:31", + "branch": null, + "member": { + "id": 6805, + "short_name": "Giulia B.", + "card_code": "GB2026", + "account_status": "attivo", + "card_active": true, + "card_expires_at": null, + "card_expired": false, + "has_photo": false, + "photo_url": null + }, + "guest": null, + "warning": null, + "message": "Benvenuto/a, Giulia B.: presenza registrata." + }, + "meta": {}, + "error": null +} diff --git a/app/src/test/resources/server090/patrons/att_member_and_card.json b/app/src/test/resources/server090/patrons/att_member_and_card.json new file mode 100644 index 0000000..979cdd9 --- /dev/null +++ b/app/src/test/resources/server090/patrons/att_member_and_card.json @@ -0,0 +1,13 @@ +{ + "data": null, + "meta": { + "field": "user_id", + "fields": { + "user_id": "Indica la tessera oppure il socio, non entrambi." + } + }, + "error": { + "code": "validation", + "message": "Indica la tessera oppure il socio, non entrambi." + } +} diff --git a/app/src/test/resources/server090/patrons/att_member_not_found.json b/app/src/test/resources/server090/patrons/att_member_not_found.json new file mode 100644 index 0000000..62a6290 --- /dev/null +++ b/app/src/test/resources/server090/patrons/att_member_not_found.json @@ -0,0 +1,8 @@ +{ + "data": null, + "meta": {}, + "error": { + "code": "member_not_found", + "message": "Utente non trovato" + } +} diff --git a/app/src/test/resources/server090/patrons/health_092.json b/app/src/test/resources/server090/patrons/health_092.json new file mode 100644 index 0000000..afb04ed --- /dev/null +++ b/app/src/test/resources/server090/patrons/health_092.json @@ -0,0 +1,87 @@ +{ + "data": { + "name": "Pinakes", + "logo": null, + "version": "0.9.2", + "api_version": "v1", + "features": { + "catalog": true, + "loans": true, + "reservations": true, + "wishlist": true, + "messages": true, + "notifications": true, + "push": true, + "reviews": true, + "archives": true, + "desiderata": true, + "staff_catalog": true, + "catalog_sync": true, + "calendar_feed": true, + "automatic_fines": false, + "debt_block": false, + "consultation_only": true, + "fines": false, + "closures_calendar": true, + "circulation_rules": true, + "staff_circulation": true, + "staff_copies": true, + "staff_series": true, + "staff_merges": true, + "staff_labels": true, + "staff_imports": true, + "staff_covers": true, + "staff_desiderata": true, + "staff_archives": true, + "staff_periodicals": true, + "staff_bookclub": true, + "staff_frbr": true, + "staff_digital_library": true, + "staff_librarything": true, + "staff_attendance": true, + "staff_catalogue_cards": true, + "staff_circulation_rules": true, + "staff_closures_calendar": true, + "staff_ledger": false, + "staff_library_settings": true, + "staff_marc": true, + "staff_patrons": true, + "staff_statistics": true, + "staff_stocktake": false, + "staff_suppliers": false, + "staff_user_import": true, + "staff_attendance_member_pick": true, + "stocktake": false, + "marc": true, + "visitor_log": true, + "catalogue_cards": true, + "suppliers": false, + "rda_types": false, + "patron_cards": true, + "user_import": true, + "patron_photos": true, + "automatic_card_expiry": true, + "loss_penalties": false, + "attendance_retention": false + }, + "catalogue_mode": false, + "loan_approval_required": true, + "app_access_enabled": true, + "registration_enabled": true, + "registration": { + "require_cognome": true, + "require_telefono": true, + "require_indirizzo": true, + "show_sesso": true, + "show_cod_fiscale": true, + "custom_fields": [] + }, + "private_mode": false, + "vapid_public_key": "BCWaDt1H8Gij8dwivEJMu0zLyNKv605jy0pQmLYK9I9sxrmG6jPCJuugCmwXvTFOLvHidWv9pV8pCyxAYbxALJU" + }, + "meta": { + "https": false, + "warning": "insecure_transport" + }, + "error": null +} diff --git a/i18n/de.json b/i18n/de.json index a8e4a3e..1cb396d 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1584,6 +1584,8 @@ "staff090_patrons_card_expired": "Ausweis abgelaufen am %1$s", "staff090_patrons_card_not_active": "Das Konto ist gesperrt oder abgelaufen.", "staff090_patrons_guest": "Gast erfassen", + "staff090_patrons_member_pick": "Mitglied suchen", + "staff090_patrons_member_pick_title": "Mitglied ohne Ausweis", "staff090_patrons_guest_name": "Name (optional)", "staff090_patrons_guest_reason": "Grund des Besuchs (optional)", "staff090_patrons_guest_save": "Besuch erfassen", @@ -1637,6 +1639,7 @@ "staff090_patrons_err_card_not_renewable": "Dieses Konto hat keinen Ausweis zum Verlängern.", "staff090_patrons_err_amount_exceeds_balance": "Der Betrag übersteigt die Schuld des Lesers.", "staff090_patrons_err_card_not_found": "Ausweis nicht erkannt", + "staff090_patrons_err_member_not_found": "Mitglied nicht gefunden oder für dieses Konto nicht erreichbar.", "staff090_patrons_err_not_started": "Starte den Import, bevor Zeilen verarbeitet werden.", "staff090_patrons_err_already_started": "Dieser Import wurde bereits gestartet.", "staff090_patrons_err_job_cancelled": "Der Import wurde während der Verarbeitung abgebrochen.", diff --git a/i18n/en.json b/i18n/en.json index 067e31c..734cf9e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1584,6 +1584,8 @@ "staff090_patrons_card_expired": "Card expired on %1$s", "staff090_patrons_card_not_active": "The account is suspended or expired.", "staff090_patrons_guest": "Record a guest", + "staff090_patrons_member_pick": "Find member", + "staff090_patrons_member_pick_title": "Member without the card", "staff090_patrons_guest_name": "Name (optional)", "staff090_patrons_guest_reason": "Reason for the visit (optional)", "staff090_patrons_guest_save": "Record visit", @@ -1637,6 +1639,7 @@ "staff090_patrons_err_card_not_renewable": "This account has no library card to renew.", "staff090_patrons_err_amount_exceeds_balance": "The amount is more than the patron owes.", "staff090_patrons_err_card_not_found": "Card not recognised", + "staff090_patrons_err_member_not_found": "Member not found, or not reachable from this account.", "staff090_patrons_err_not_started": "Start the import before processing the rows.", "staff090_patrons_err_already_started": "This import has already started.", "staff090_patrons_err_job_cancelled": "The import was cancelled while rows were being processed.", diff --git a/i18n/fr.json b/i18n/fr.json index 997a539..2c8fa74 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1584,6 +1584,8 @@ "staff090_patrons_card_expired": "Carte expirée le %1$s", "staff090_patrons_card_not_active": "Le compte est suspendu ou expiré.", "staff090_patrons_guest": "Enregistrer un visiteur", + "staff090_patrons_member_pick": "Rechercher un membre", + "staff090_patrons_member_pick_title": "Membre sans sa carte", "staff090_patrons_guest_name": "Nom (facultatif)", "staff090_patrons_guest_reason": "Motif de la visite (facultatif)", "staff090_patrons_guest_save": "Enregistrer la visite", @@ -1637,6 +1639,7 @@ "staff090_patrons_err_card_not_renewable": "Ce compte n'a pas de carte à renouveler.", "staff090_patrons_err_amount_exceeds_balance": "Le montant dépasse ce que doit le lecteur.", "staff090_patrons_err_card_not_found": "Carte non reconnue", + "staff090_patrons_err_member_not_found": "Membre introuvable ou hors de portée de ce compte.", "staff090_patrons_err_not_started": "Lancez l'import avant de traiter les lignes.", "staff090_patrons_err_already_started": "Cet import a déjà commencé.", "staff090_patrons_err_job_cancelled": "L'import a été annulé pendant le traitement.", diff --git a/i18n/it.json b/i18n/it.json index 713e605..0231213 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -1584,6 +1584,8 @@ "staff090_patrons_card_expired": "Tessera scaduta il %1$s", "staff090_patrons_card_not_active": "L'account è sospeso o scaduto.", "staff090_patrons_guest": "Registra ospite", + "staff090_patrons_member_pick": "Cerca socio", + "staff090_patrons_member_pick_title": "Socio senza tessera", "staff090_patrons_guest_name": "Nome (facoltativo)", "staff090_patrons_guest_reason": "Motivo della visita (facoltativo)", "staff090_patrons_guest_save": "Registra presenza", @@ -1637,6 +1639,7 @@ "staff090_patrons_err_card_not_renewable": "Questo account non ha una tessera da rinnovare.", "staff090_patrons_err_amount_exceeds_balance": "L'importo supera quanto dovuto dal lettore.", "staff090_patrons_err_card_not_found": "Tessera non riconosciuta", + "staff090_patrons_err_member_not_found": "Socio non trovato o non raggiungibile da questo account.", "staff090_patrons_err_not_started": "Avvia l'importazione prima di elaborare le righe.", "staff090_patrons_err_already_started": "Questa importazione è già stata avviata.", "staff090_patrons_err_job_cancelled": "L'importazione è stata annullata durante l'elaborazione.",