From 4781a695e1e781cc9c2b0af835eb05aa8bbe688d Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Fri, 6 Nov 2026 15:22:28 +0100 Subject: [PATCH] Release 1.12.0 (versionCode 26): the same-day handover switch Library settings -> Loans gains the web's Closures calendar section with same_day_handover_after_closure (Pinakes 0.9.0), sent only when changed and only when the server sends it. With #58 (usable return day and server lateness) this is the 1.12.0 release. --- STATUS.md | 7 ++ app/build.gradle.kts | 4 +- .../staff/s090/library/LibraryForms.kt | 3 +- .../s090/library/LibrarySettingsScreen.kt | 3 + .../app/server090/Staff090LibraryFormsTest.kt | 22 ++++ .../library/s090_library_loans_staff_091.json | 112 ++++++++++++++++++ i18n/de.json | 3 + i18n/en.json | 3 + i18n/fr.json | 3 + i18n/it.json | 3 + 10 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 app/src/test/resources/server090/library/s090_library_loans_staff_091.json diff --git a/STATUS.md b/STATUS.md index 73de51e..dcfa72c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,5 +1,12 @@ # Pinakes Android — Build Status +## 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. + +- **Loans show the usable return day** ([#58](https://github.com/fabiodalez-dev/Pinakes-Android/pull/58)): reader and staff loans read `effective_due_at` (the first open day after a due date closed afterwards) and `is_overdue` (lateness in the library timezone and the copy branch calendar). The date shown, the "late" state and the active/overdue grouping follow them, so a loan due on a closure is not shown late while the library is closed; `due_at` keeps the stored date. +- **Same-day handover after a closure**: Library settings → Loans gains the web's *Closures calendar* section with the `same_day_handover_after_closure` switch and its help (when on, another reader can receive the book on the first open day after a closed due day). It is sent only when changed, and only when the server sends it; Staff can change it, as on the web. + ## 1.11.0 (versionCode 25): Pinakes 0.9.0, and its whole admin from the staff area Pairs with Pinakes 0.9.0-rc.2 (Mobile API 1.10.0 for the staff areas, the copy editor's consultation switch and `patron_type` on the profile; Mobile API 1.9.0 for `closed_dates`, `loanable`, `reservations_enabled`, `/me/movements` and the new 409 codes; Mobile API 1.9.1 for the `/health` toggles `fines`, `closures_calendar` and `circulation_rules`). Works with older servers: every field is optional, the three flags default to off, and their absence keeps the 1.10.0 behaviour. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d8d5c2..e0c1468 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 = 25 - versionName = "1.11.0" + versionCode = 26 + versionName = "1.12.0" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibraryForms.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibraryForms.kt index b93281b..e88302b 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibraryForms.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibraryForms.kt @@ -122,7 +122,7 @@ enum class LoanFieldKind { INT, BOOL, CHOICE, MONEY, TIMEZONE } /** One field of the loans tab, in the web page's order. */ data class LoanField(val key: String, val kind: LoanFieldKind, val section: LoanSection) -enum class LoanSection { APPROVAL, DURATION, PICKUP, CARD, RENEWALS, LIMITS, RECALLS, FINES } +enum class LoanSection { APPROVAL, DURATION, CLOSURES, PICKUP, CARD, RENEWALS, LIMITS, RECALLS, FINES } object LoansForm { /** @@ -137,6 +137,7 @@ object LoansForm { LoanField("loan_duration_days", LoanFieldKind.INT, LoanSection.DURATION), LoanField("max_loan_duration_days", LoanFieldKind.INT, LoanSection.DURATION), LoanField("app_timezone", LoanFieldKind.TIMEZONE, LoanSection.DURATION), + LoanField("same_day_handover_after_closure", LoanFieldKind.BOOL, LoanSection.CLOSURES), LoanField("pickup_expiry_days", LoanFieldKind.INT, LoanSection.PICKUP), LoanField("pickup_reminder_days", LoanFieldKind.INT, LoanSection.PICKUP), LoanField("card_expiry_warning_days", LoanFieldKind.INT, LoanSection.CARD), diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibrarySettingsScreen.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibrarySettingsScreen.kt index b953d0f..5c984a9 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibrarySettingsScreen.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/s090/library/LibrarySettingsScreen.kt @@ -464,6 +464,7 @@ internal fun CardsTab(c: Staff090LibraryCards, state: LibrarySettingsUiState, on private fun loanSectionTitle(s: LoanSection) = when (s) { LoanSection.APPROVAL -> R.string.staff090_library_loans_approval LoanSection.DURATION -> R.string.staff090_library_loans_duration + LoanSection.CLOSURES -> R.string.staff090_library_loans_closures LoanSection.PICKUP -> R.string.staff090_library_loans_pickup LoanSection.CARD -> R.string.staff090_library_loans_card LoanSection.RENEWALS -> R.string.staff090_library_loans_renewals @@ -477,6 +478,7 @@ internal fun loanFieldLabel(key: String) = when (key) { "loan_duration_days" -> R.string.staff090_library_loan_duration "max_loan_duration_days" -> R.string.staff090_library_loan_max_duration "app_timezone" -> R.string.staff090_library_loan_timezone + "same_day_handover_after_closure" -> R.string.staff090_library_loan_same_day_handover "pickup_expiry_days" -> R.string.staff090_library_loan_pickup_days "pickup_reminder_days" -> R.string.staff090_library_loan_pickup_reminder "card_expiry_warning_days" -> R.string.staff090_library_loan_card_warning @@ -501,6 +503,7 @@ private fun loanFieldHelp(key: String): Int? = when (key) { "loan_duration_days" -> R.string.staff090_library_loan_duration_help "max_loan_duration_days" -> R.string.staff090_library_loan_max_duration_help "app_timezone" -> R.string.staff090_library_loan_timezone_help + "same_day_handover_after_closure" -> R.string.staff090_library_loan_same_day_handover_help "pickup_expiry_days" -> R.string.staff090_library_loan_pickup_days_help "pickup_reminder_days" -> R.string.staff090_library_loan_pickup_reminder_help "card_expiry_warning_days" -> R.string.staff090_library_loan_card_warning_help diff --git a/app/src/test/java/com/pinakes/app/server090/Staff090LibraryFormsTest.kt b/app/src/test/java/com/pinakes/app/server090/Staff090LibraryFormsTest.kt index c3b19fe..4cb0900 100644 --- a/app/src/test/java/com/pinakes/app/server090/Staff090LibraryFormsTest.kt +++ b/app/src/test/java/com/pinakes/app/server090/Staff090LibraryFormsTest.kt @@ -22,6 +22,8 @@ import com.pinakes.app.ui.screens.staff.s090.library.CustomForms import com.pinakes.app.ui.screens.staff.s090.library.IntInput import com.pinakes.app.ui.screens.staff.s090.library.LibraryMoney import com.pinakes.app.ui.screens.staff.s090.library.LibrarySettingsUiState +import com.pinakes.app.ui.screens.staff.s090.library.LoanFieldKind +import com.pinakes.app.ui.screens.staff.s090.library.LoanSection import com.pinakes.app.ui.screens.staff.s090.library.LoansForm import com.pinakes.app.ui.screens.staff.s090.library.ModulesForm import com.pinakes.app.ui.screens.staff.s090.library.RuleNumber @@ -139,6 +141,26 @@ class Staff090LibraryFormsTest { assertFalse(LoansForm.editable(l, "fine_grace_mode")) } + @Test fun theSameDayHandoverSwitchFollowsTheClosuresCalendarAndIsSentWhenChanged() { + // Pinakes 0.9.0+ sends the setting; staff may change it (it is not administrator-only). + val l = data("s090_library_loans_staff_091", Staff090LibraryLoans.serializer()) + val field = LoansForm.fields(l).single { it.key == "same_day_handover_after_closure" } + assertEquals(LoanFieldKind.BOOL, field.kind) + assertEquals(LoanSection.CLOSURES, field.section) + val keys = LoansForm.fields(l).map { it.key } + assertTrue(keys.indexOf("app_timezone") < keys.indexOf("same_day_handover_after_closure")) + assertTrue(keys.indexOf("same_day_handover_after_closure") < keys.indexOf("pickup_expiry_days")) + assertTrue(LoansForm.editable(l, "same_day_handover_after_closure")) + val original = LoansForm.initial(l, it) + assertEquals("false", original["same_day_handover_after_closure"]) + assertEquals("""{"same_day_handover_after_closure":true}""", LoansForm.body(l, original, original + ("same_day_handover_after_closure" to "true")).toString()) + // A server before 0.9.0 does not send it: no switch, and nothing about it in the body. + val old = data("s090_library_loans_staff", Staff090LibraryLoans.serializer()) + assertFalse(LoansForm.fields(old).any { it.key == "same_day_handover_after_closure" }) + val oldOriginal = LoansForm.initial(old, it) + assertNull(LoansForm.body(old, oldOriginal, oldOriginal + ("same_day_handover_after_closure" to "true"))) + } + @Test fun loanValidationUsesTheServerLimits() { val l = data("s090_library_loans_admin", Staff090LibraryLoans.serializer()) val values = LoansForm.initial(l, it) diff --git a/app/src/test/resources/server090/library/s090_library_loans_staff_091.json b/app/src/test/resources/server090/library/s090_library_loans_staff_091.json new file mode 100644 index 0000000..7eb5c8c --- /dev/null +++ b/app/src/test/resources/server090/library/s090_library_loans_staff_091.json @@ -0,0 +1,112 @@ +{ + "data": { + "can_configure": false, + "currency": "EUR", + "settings": { + "loan_duration_days": 30, + "pickup_expiry_days": 3, + "pickup_reminder_days": 4, + "card_expiry_warning_days": 0, + "max_renewals": 3, + "max_active_loans_per_user": 0, + "max_loan_duration_days": 90, + "recall_interval_days": 7, + "recall_max_count": 3, + "auto_approve_requests": false, + "allow_multiple_loans_same_book": false, + "recall_auto_enabled": false, + "same_day_handover_after_closure": false, + "app_timezone": "Europe/Rome", + "daily_fine_cents": null, + "fine_max_per_loan_cents": null, + "debt_block_threshold_cents": null, + "fine_grace_days": 0, + "fine_grace_mode": "threshold", + "card_renewal_base": "today", + "fine_charged_email": false + }, + "editable_fields": [ + "loan_duration_days", + "pickup_expiry_days", + "pickup_reminder_days", + "card_expiry_warning_days", + "max_renewals", + "max_active_loans_per_user", + "max_loan_duration_days", + "recall_interval_days", + "recall_max_count", + "auto_approve_requests", + "allow_multiple_loans_same_book", + "recall_auto_enabled", + "same_day_handover_after_closure", + "app_timezone" + ], + "admin_only_fields": [ + "daily_fine_cents", + "fine_max_per_loan_cents", + "debt_block_threshold_cents", + "fine_grace_days", + "fine_grace_mode", + "card_renewal_base", + "fine_charged_email" + ], + "limits": { + "loan_duration_days": { + "min": 1, + "max": 3650 + }, + "pickup_expiry_days": { + "min": 1, + "max": 30 + }, + "pickup_reminder_days": { + "min": 0, + "max": 30 + }, + "card_expiry_warning_days": { + "min": 0, + "max": 365 + }, + "max_renewals": { + "min": 0, + "max": 100 + }, + "max_active_loans_per_user": { + "min": 0, + "max": 1000 + }, + "max_loan_duration_days": { + "min": 1, + "max": 3650 + }, + "recall_interval_days": { + "min": 1, + "max": 365 + }, + "recall_max_count": { + "min": 1, + "max": 50 + }, + "fine_grace_days": { + "min": 0, + "max": 365 + }, + "money_max_cents": { + "min": 0, + "max": 999999 + } + }, + "options": { + "fine_grace_mode": [ + "threshold", + "deduct" + ], + "card_renewal_base": [ + "today", + "expiry" + ] + } + }, + "meta": {}, + "error": null +} diff --git a/i18n/de.json b/i18n/de.json index b7490a3..a8e4a3e 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1794,6 +1794,8 @@ "staff090_library_loan_max_renewals_help": "0 schaltet Verlängerungen ab.", "staff090_library_loan_multiple_copies": "Mehrere Exemplare desselben Titels", "staff090_library_loan_multiple_copies_help": "Ein Leser darf dasselbe Buch mehrfach ausleihen, jede Ausleihe auf einem anderen Exemplar.", + "staff090_library_loan_same_day_handover": "Übergabe am selben Tag nach einer Schließung", + "staff090_library_loan_same_day_handover_help": "Wenn eine Ausleihe an einem Schließtag fällig ist, kommt das Buch am ersten Öffnungstag zurück. Wenn aktiv, kann eine andere Person es noch am selben Tag erhalten; wenn inaktiv, bleibt das Buch den ganzen Tag belegt.", "staff090_library_loan_pickup_days": "Tage zur Abholung einer genehmigten Ausleihe", "staff090_library_loan_pickup_days_help": "Danach verfällt die genehmigte Ausleihe.", "staff090_library_loan_pickup_reminder": "Abholerinnerung (Tage vorher)", @@ -1812,6 +1814,7 @@ "staff090_library_loans_fines": "Gebühren und Schuldensperre", "staff090_library_loans_limits": "Grenzen", "staff090_library_loans_pickup": "Abholung", + "staff090_library_loans_closures": "Schließtage-Kalender", "staff090_library_loans_recalls": "Mahnungen", "staff090_library_loans_renewals": "Verlängerungen", "staff090_library_loans_section": "Ausleihe", diff --git a/i18n/en.json b/i18n/en.json index 81de415..067e31c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1794,6 +1794,8 @@ "staff090_library_loan_max_renewals_help": "0 turns renewals off.", "staff090_library_loan_multiple_copies": "Several copies of the same title", "staff090_library_loan_multiple_copies_help": "A patron may borrow the same book more than once, each loan on a different copy.", + "staff090_library_loan_same_day_handover": "Same-day handover after a closure", + "staff090_library_loan_same_day_handover_help": "When a loan is due on a closed day, the book comes back on the first open day. When on, another reader can receive it that same day; when off, the book stays taken for that whole day.", "staff090_library_loan_pickup_days": "Days to pick up an approved loan", "staff090_library_loan_pickup_days_help": "After that the approved loan expires.", "staff090_library_loan_pickup_reminder": "Pickup reminder (days before)", @@ -1812,6 +1814,7 @@ "staff090_library_loans_fines": "Fines and debt block", "staff090_library_loans_limits": "Limits", "staff090_library_loans_pickup": "Pickup", + "staff090_library_loans_closures": "Closures calendar", "staff090_library_loans_recalls": "Overdue reminders", "staff090_library_loans_renewals": "Renewals", "staff090_library_loans_section": "Loans", diff --git a/i18n/fr.json b/i18n/fr.json index b701981..997a539 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1794,6 +1794,8 @@ "staff090_library_loan_max_renewals_help": "0 désactive les renouvellements.", "staff090_library_loan_multiple_copies": "Plusieurs exemplaires du même titre", "staff090_library_loan_multiple_copies_help": "Un lecteur peut emprunter le même livre plusieurs fois, chaque prêt sur un exemplaire différent.", + "staff090_library_loan_same_day_handover": "Remise le jour même après une fermeture", + "staff090_library_loan_same_day_handover_help": "Lorsqu'un prêt arrive à échéance un jour de fermeture, le livre revient le premier jour d'ouverture. Si activé, un autre lecteur peut le recevoir le jour même ; si désactivé, le livre reste indisponible toute cette journée.", "staff090_library_loan_pickup_days": "Jours pour retirer un prêt approuvé", "staff090_library_loan_pickup_days_help": "Passé ce délai le prêt approuvé expire.", "staff090_library_loan_pickup_reminder": "Rappel de retrait (jours avant)", @@ -1812,6 +1814,7 @@ "staff090_library_loans_fines": "Amendes et blocage pour dette", "staff090_library_loans_limits": "Limites", "staff090_library_loans_pickup": "Retrait", + "staff090_library_loans_closures": "Calendrier des fermetures", "staff090_library_loans_recalls": "Relances", "staff090_library_loans_renewals": "Renouvellements", "staff090_library_loans_section": "Prêts", diff --git a/i18n/it.json b/i18n/it.json index 2c10996..713e605 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -1794,6 +1794,8 @@ "staff090_library_loan_max_renewals_help": "0 disattiva i rinnovi.", "staff090_library_loan_multiple_copies": "Più copie dello stesso titolo", "staff090_library_loan_multiple_copies_help": "Un lettore può avere più prestiti dello stesso libro, ciascuno su una copia diversa.", + "staff090_library_loan_same_day_handover": "Consegna nello stesso giorno dopo una chiusura", + "staff090_library_loan_same_day_handover_help": "Quando un prestito scade in un giorno di chiusura, il libro torna il primo giorno di apertura. Se attivo, un altro lettore può riceverlo quello stesso giorno; se disattivo, il libro resta occupato per tutto quel giorno.", "staff090_library_loan_pickup_days": "Giorni per il ritiro del prestito approvato", "staff090_library_loan_pickup_days_help": "Dopo questo termine il prestito approvato scade.", "staff090_library_loan_pickup_reminder": "Promemoria di ritiro (giorni prima)", @@ -1812,6 +1814,7 @@ "staff090_library_loans_fines": "Multe e blocco per debito", "staff090_library_loans_limits": "Limiti", "staff090_library_loans_pickup": "Ritiro", + "staff090_library_loans_closures": "Calendario chiusure", "staff090_library_loans_recalls": "Solleciti", "staff090_library_loans_renewals": "Rinnovi", "staff090_library_loans_section": "Prestiti",