From a5fd001f2164759a07ffa02ce0707305b0dd57a8 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 15 Oct 2026 00:39:00 +0200 Subject: [PATCH] Receive a wanted title with a clear action, then open its new copy On a wanted title the trailing "Ricevuto" (Received) read as a status, so a title still wanted with no copies looked already received, and one tap added a copy with no question. The button now says "Ricevi" (Receive) and asks before adding the copy. Receiving a wanted title or an offer creates a copy that still needs its code and place. The app now opens the record, scrolls to the copies and opens that copy's editor straight away, through an optional copy argument on the book editor route. --- .../assets/fixtures/desiderata_wanted.json | 1 + .../desiderata_wanted_77_received.json | 1 + .../fixtures/staff_book_5_received.json | 1 + .../fixtures/staff_book_77_received.json | 1 + .../app/staffe2e/DesiderataStaffFlowTest.kt | 44 +++++++++++++++-- .../app/ui/navigation/PinakesNavHost.kt | 11 ++++- .../com/pinakes/app/ui/navigation/Routes.kt | 7 ++- .../ui/screens/staff/StaffBookEditScreen.kt | 2 + .../screens/staff/StaffBookEditViewModel.kt | 8 ++++ .../staff/content/StaffCopiesSection.kt | 18 ++++++- .../staff/plugins/StaffDesiderataScreen.kt | 47 ++++++++++++++++--- i18n/de.json | 4 +- i18n/en.json | 4 +- i18n/fr.json | 4 +- i18n/it.json | 4 +- 15 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 app/src/androidTest/assets/fixtures/desiderata_wanted.json create mode 100644 app/src/androidTest/assets/fixtures/desiderata_wanted_77_received.json create mode 100644 app/src/androidTest/assets/fixtures/staff_book_5_received.json create mode 100644 app/src/androidTest/assets/fixtures/staff_book_77_received.json diff --git a/app/src/androidTest/assets/fixtures/desiderata_wanted.json b/app/src/androidTest/assets/fixtures/desiderata_wanted.json new file mode 100644 index 0000000..d706fd4 --- /dev/null +++ b/app/src/androidTest/assets/fixtures/desiderata_wanted.json @@ -0,0 +1 @@ +{"data":[{"id":77,"title":"Non diciamo no da sole","author":"Sara Ahmed","publisher":"Fandango Libri","isbn":"9781256361909","cover_url":null,"open_offers":0}],"meta":{"next_cursor":null},"error":null} diff --git a/app/src/androidTest/assets/fixtures/desiderata_wanted_77_received.json b/app/src/androidTest/assets/fixtures/desiderata_wanted_77_received.json new file mode 100644 index 0000000..7f4da41 --- /dev/null +++ b/app/src/androidTest/assets/fixtures/desiderata_wanted_77_received.json @@ -0,0 +1 @@ +{"data":{"book_id":77,"copy_id":901,"offer_id":55},"meta":{},"error":null} diff --git a/app/src/androidTest/assets/fixtures/staff_book_5_received.json b/app/src/androidTest/assets/fixtures/staff_book_5_received.json new file mode 100644 index 0000000..749dded --- /dev/null +++ b/app/src/androidTest/assets/fixtures/staff_book_5_received.json @@ -0,0 +1 @@ +{"data": {"id": 5, "title": "Orlando", "subtitle": "A Biography", "isbn13": "9780156701600", "year": 1928, "pages": 333, "language": "English", "media_type": "libro", "copies_total": 4, "copies_available": 1, "genre": {"id": 11, "name": "Romanzo"}, "authors": [{"id": 1, "name": "Virginia Woolf", "role": "principale"}, {"id": 7, "name": "Nadia Fusini", "role": "traduttore"}], "publishers": [{"id": 4, "name": "Hogarth"}], "location": {"shelf_id": 7, "shelf_code": "A", "level_id": 70, "level_number": 1, "position": 3, "label": "A.1.3"}, "copies": [{"id": 11, "inventory_number": "INV-0011", "status": "disponibile", "notes": null}, {"id": 12, "inventory_number": "INV-0012", "status": "prestato", "notes": "Spine worn"}, {"id": 77, "inventory_number": "LIB-5-C77", "status": "disponibile", "notes": null}, {"id": 78, "inventory_number": "LIB-5-C78", "status": "disponibile", "notes": null}], "is_wanted": false}, "meta": {}, "error": null} \ No newline at end of file diff --git a/app/src/androidTest/assets/fixtures/staff_book_77_received.json b/app/src/androidTest/assets/fixtures/staff_book_77_received.json new file mode 100644 index 0000000..465cc3c --- /dev/null +++ b/app/src/androidTest/assets/fixtures/staff_book_77_received.json @@ -0,0 +1 @@ +{"data": {"id": 77, "title": "Non diciamo no da sole", "subtitle": "A Biography", "isbn13": "9780156701600", "year": 1928, "pages": 333, "language": "English", "media_type": "libro", "copies_total": 3, "copies_available": 1, "genre": {"id": 11, "name": "Romanzo"}, "authors": [{"id": 1, "name": "Virginia Woolf", "role": "principale"}, {"id": 7, "name": "Nadia Fusini", "role": "traduttore"}], "publishers": [{"id": 4, "name": "Hogarth"}], "location": {"shelf_id": 7, "shelf_code": "A", "level_id": 70, "level_number": 1, "position": 3, "label": "A.1.3"}, "copies": [{"id": 11, "inventory_number": "INV-0011", "status": "disponibile", "notes": null}, {"id": 12, "inventory_number": "INV-0012", "status": "prestato", "notes": "Spine worn"}, {"id": 901, "inventory_number": "LIB-77-C901", "status": "disponibile", "notes": null}], "is_wanted": false}, "meta": {}, "error": null} \ No newline at end of file diff --git a/app/src/androidTest/java/com/pinakes/app/staffe2e/DesiderataStaffFlowTest.kt b/app/src/androidTest/java/com/pinakes/app/staffe2e/DesiderataStaffFlowTest.kt index d9b92b5..d2cfffd 100644 --- a/app/src/androidTest/java/com/pinakes/app/staffe2e/DesiderataStaffFlowTest.kt +++ b/app/src/androidTest/java/com/pinakes/app/staffe2e/DesiderataStaffFlowTest.kt @@ -1,7 +1,12 @@ package com.pinakes.app.staffe2e import com.pinakes.app.R +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onLast +import androidx.compose.ui.test.performClick import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test /** Hub → Donation offers: receive an offer for a wanted title, receive one into a picked record, a closed offer. */ @@ -18,21 +23,52 @@ class DesiderataStaffFlowTest : AppFlowTest() { } @Test fun receiveAnOfferForAWantedTitle() { - open { json("POST staff/desiderata/offers/41/receive", "offer_41_received") } + open { + json("POST staff/desiderata/offers/41/receive", "offer_41_received") + json("GET staff/books/5", "staff_book_5_received") + } clickTag("offer-41") clickTag("offer-receive") assertEquals("{}", lastBody("POST", "staff/desiderata/offers/41/receive")) - waitText(text(R.string.staff_offer_received)) + // The new copy opens on the record, ready for its code and place. + waitFor(hasTestTag("copy-save")) } @Test fun anOfferWithoutATitleIsReceivedIntoAPickedRecord() { - open { json("POST staff/desiderata/offers/42/receive", "offer_42_received") } + open { + json("POST staff/desiderata/offers/42/receive", "offer_42_received") + json("GET staff/books/5", "staff_book_5_received") + } clickTag("offer-42") clickTag("offer-receive") waitText(text(R.string.staff_offer_receive_into)) clickTag("pick-5") assertEquals("""{"received_book_id":5}""", lastBody("POST", "staff/desiderata/offers/42/receive")) - waitText(text(R.string.staff_offer_received)) + waitFor(hasTestTag("copy-save")) + } + + @Test fun aWantedTitleIsReceivedOnlyAfterConfirming() { + open { + json("GET staff/desiderata/wanted", "desiderata_wanted") + json("POST staff/desiderata/wanted/77/receive", "desiderata_wanted_77_received", 201) + json("GET staff/books/77", "staff_book_77_received") + } + click(text(R.string.staff_offers_wanted)) + waitText("Non diciamo no da sole") + // The row's button is an action, not a status: a tap only asks. + click(text(R.string.staff_wanted_mark_received)) + waitText(text(R.string.staff_wanted_receive_confirm_title)) + assertTrue(api.recorded("POST", "staff/desiderata/wanted/77/receive").isEmpty()) + click(text(R.string.action_cancel)) + assertTrue(api.recorded("POST", "staff/desiderata/wanted/77/receive").isEmpty()) + click(text(R.string.staff_wanted_mark_received)) + waitText(text(R.string.staff_wanted_receive_confirm_title)) + compose.onAllNodesWithText(text(R.string.staff_wanted_mark_received)).onLast().performClick() + waitRequest("POST", "staff/desiderata/wanted/77/receive") + assertEquals(1, api.recorded("POST", "staff/desiderata/wanted/77/receive").size) + // Received: the record opens on the new copy's editor, to give it its code and place. + waitFor(hasTestTag("copy-save")) + waitText("LIB-77-C901") } @Test fun aClosedOfferSaysSo() { diff --git a/app/src/main/java/com/pinakes/app/ui/navigation/PinakesNavHost.kt b/app/src/main/java/com/pinakes/app/ui/navigation/PinakesNavHost.kt index ddf9d5f..d37fca1 100644 --- a/app/src/main/java/com/pinakes/app/ui/navigation/PinakesNavHost.kt +++ b/app/src/main/java/com/pinakes/app/ui/navigation/PinakesNavHost.kt @@ -316,7 +316,11 @@ fun PinakesNavHost(navController: NavHostController = rememberNavController()) { } // ---- Staff plugin content (Mobile API 1.7.0) ---- composable(Routes.STAFF_DESIDERATA, enterTransition = slideIn, popExitTransition = slideOut) { - StaffDesiderataScreen(onNavigateUp = { navController.popBackStack() }, onOpenBook = { navController.navigate(Routes.staffBookEdit(it)) }) + StaffDesiderataScreen( + onNavigateUp = { navController.popBackStack() }, + onOpenBook = { navController.navigate(Routes.staffBookEdit(it)) }, + onOpenCopy = { book, copy -> navController.navigate(Routes.staffBookEdit(book, copy)) }, + ) } composable(Routes.STAFF_ARCHIVES, enterTransition = slideIn, popExitTransition = slideOut) { StaffArchivesScreen( @@ -415,7 +419,10 @@ fun PinakesNavHost(navController: NavHostController = rememberNavController()) { } composable( Routes.STAFF_BOOK_EDIT, - arguments = listOf(navArgument(Routes.ARG_BOOK_ID) { type = NavType.IntType }), + arguments = listOf( + navArgument(Routes.ARG_BOOK_ID) { type = NavType.IntType }, + navArgument(Routes.ARG_COPY_ID) { type = NavType.IntType; defaultValue = 0 }, + ), enterTransition = slideIn, popExitTransition = slideOut, ) { StaffBookEditScreen( diff --git a/app/src/main/java/com/pinakes/app/ui/navigation/Routes.kt b/app/src/main/java/com/pinakes/app/ui/navigation/Routes.kt index 18d045c..34f627d 100644 --- a/app/src/main/java/com/pinakes/app/ui/navigation/Routes.kt +++ b/app/src/main/java/com/pinakes/app/ui/navigation/Routes.kt @@ -64,8 +64,11 @@ object Routes { const val ARG_ISBN = "isbn" const val STAFF_BOOK_NEW = "staff/books/new?isbn={isbn}" fun staffBookNew(isbn: String = ""): String = "staff/books/new?isbn=${Uri.encode(isbn)}" - const val STAFF_BOOK_EDIT = "staff/books/{bookId}/edit" - fun staffBookEdit(bookId: Int): String = "staff/books/$bookId/edit" + /** Optional: the copy whose editor the book page opens straight away (a copy just received). */ + const val ARG_COPY_ID = "copyId" + const val STAFF_BOOK_EDIT = "staff/books/{bookId}/edit?copy={copyId}" + fun staffBookEdit(bookId: Int, copyId: Int? = null): String = + "staff/books/$bookId/edit" + (copyId?.takeIf { it > 0 }?.let { "?copy=$it" } ?: "") const val STAFF_AUTHORS = "staff/authors" const val STAFF_PUBLISHERS = "staff/publishers" const val ARG_RECORD_ID = "recordId" diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditScreen.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditScreen.kt index 816ddd3..e33a4ba 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditScreen.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditScreen.kt @@ -306,6 +306,8 @@ private fun BookForm(state: StaffBookEditUiState, vm: StaffBookEditViewModel) { onBookRefreshed = vm::applyCopies, onMessage = vm::showMessage, onForbidden = vm::markForbidden, + openCopyId = vm.openCopyId, + onCopyOpened = vm::consumeOpenCopy, ) // Digital Library plugin (`staff_digital_library`): eBook / audiobook upload. com.pinakes.app.ui.screens.staff.content.StaffDigitalSection(onMessage = vm::showMessage, onForbidden = vm::markForbidden) diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditViewModel.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditViewModel.kt index 6df3fa2..f441eec 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditViewModel.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditViewModel.kt @@ -121,6 +121,14 @@ class StaffBookEditViewModel @Inject constructor( private var bookId: Int = savedStateHandle.get(Routes.ARG_BOOK_ID) ?: 0 private val isbnArg: String = savedStateHandle.get(Routes.ARG_ISBN).orEmpty() + /** The copy to open on arrival (a copy just received, waiting for its code and place); 0 = none. */ + var openCopyId: Int = savedStateHandle.get(Routes.ARG_COPY_ID) ?: 0 + private set + + fun consumeOpenCopy() { + openCopyId = 0 + } + private val _state = MutableStateFlow(StaffBookEditUiState(bookId = bookId)) val state: StateFlow = _state.asStateFlow() diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/content/StaffCopiesSection.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/content/StaffCopiesSection.kt index dd3a9d4..69ba810 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/content/StaffCopiesSection.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/content/StaffCopiesSection.kt @@ -34,6 +34,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.runtime.remember import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -219,7 +223,7 @@ internal fun copyStateLabel(copy: StaffCopy, lookups: StaffLookups): String = * (`staff_copies`); label buttons only with `staff_labels`. Messages go to [onMessage] (the * editor's snackbar); counter changes to [onBookRefreshed]. */ -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) @Composable fun StaffCopiesSection( copies: List, @@ -228,6 +232,8 @@ fun StaffCopiesSection( onBookRefreshed: (StaffBook) -> Unit, onMessage: (String) -> Unit, onForbidden: () -> Unit, + openCopyId: Int = 0, + onCopyOpened: () -> Unit = {}, ) { val app: AppViewModel = hiltViewModel() val areas by app.staffAreas.collectAsStateWithLifecycle() @@ -238,11 +244,19 @@ fun StaffCopiesSection( // The editor's "Add copies" changed the total: read the copies again. LaunchedEffect(copiesTotal) { if (copiesTotal != state.copies.size) vm.reload() } LaunchedEffect(state.refreshed) { state.refreshed?.let { onBookRefreshed(it); vm.consumeRefreshed() } } + // Arriving from a receipt: scroll to the copies and open the new copy's editor at once. + val bringIntoView = remember { BringIntoViewRequester() } + LaunchedEffect(openCopyId, state.copies) { + val copy = state.copies.firstOrNull { it.id == openCopyId } ?: return@LaunchedEffect + onCopyOpened() + bringIntoView.bringIntoView() + vm.startEdit(copy) + } LaunchedEffect(state.forbidden) { if (state.forbidden) onForbidden() } val message = state.message?.resolve() LaunchedEffect(state.message) { if (!message.isNullOrBlank()) { onMessage(message); vm.consumeMessage() } } - Column(verticalArrangement = Arrangement.spacedBy(Spacing.xs), modifier = Modifier.testTag("copies-section")) { + Column(verticalArrangement = Arrangement.spacedBy(Spacing.xs), modifier = Modifier.bringIntoViewRequester(bringIntoView).testTag("copies-section")) { if (areas.copies) { state.copies.forEach { copy -> Surface( diff --git a/app/src/main/java/com/pinakes/app/ui/screens/staff/plugins/StaffDesiderataScreen.kt b/app/src/main/java/com/pinakes/app/ui/screens/staff/plugins/StaffDesiderataScreen.kt index f712222..39eeb89 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/staff/plugins/StaffDesiderataScreen.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/staff/plugins/StaffDesiderataScreen.kt @@ -107,6 +107,10 @@ data class DesiderataUiState( val forbidden: Boolean = false, val selected: StaffOffer? = null, val confirmDelete: StaffOffer? = null, + /** A wanted title waiting for "mark received" to be confirmed: it adds a copy at once. */ + val confirmReceive: StaffWantedBook? = null, + /** A receipt created this copy of this record: the screen opens its editor (code, place). */ + val openCopy: Pair? = null, val pickingBookFor: StaffOffer? = null, val busy: Boolean = false, val message: StaffMessage? = null, @@ -238,7 +242,7 @@ class StaffDesiderataViewModel @Inject constructor( val res = call(offer) _state.update { when (res) { - is ApiResult.Success -> it.copy(busy = false, selected = null, message = StaffMessage(success)) + is ApiResult.Success -> it.copy(busy = false, selected = null, message = StaffMessage(success), openCopy = receivedCopy(res.data)) is ApiResult.Failure -> it.copy(busy = false, forbidden = res.isForbidden(), message = res.toStaffMessage(R.string.staff_error_save)) } } @@ -264,15 +268,22 @@ class StaffDesiderataViewModel @Inject constructor( } } - /** "Received" on a wanted title: the copy arrived outside an offer (bought, given at the desk). */ - fun receiveWanted(book: StaffWantedBook) { + fun askReceiveWanted(book: StaffWantedBook) = _state.update { it.copy(confirmReceive = book) } + fun dismissReceiveWanted() = _state.update { it.copy(confirmReceive = null) } + + /** "Mark received" on a wanted title, once confirmed: the copy arrived outside an offer (bought, given at the desk). */ + fun receiveWanted() { + val book = _state.value.confirmReceive ?: return if (_state.value.busy) return - _state.update { it.copy(busy = true) } + _state.update { it.copy(busy = true, confirmReceive = null) } viewModelScope.launch { val res = plugins.receiveWanted(book.id) _state.update { when (res) { - is ApiResult.Success -> it.copy(busy = false, message = StaffMessage(R.string.staff_wanted_received, args = listOf(book.title))) + is ApiResult.Success -> it.copy( + busy = false, message = StaffMessage(R.string.staff_wanted_received, args = listOf(book.title)), + openCopy = res.data.copyId?.takeIf { c -> c > 0 }?.let { c -> res.data.bookId to c }, + ) is ApiResult.Failure -> it.copy( busy = false, forbidden = res.isForbidden(), message = if (res.meta?.reason == "open_offers") StaffMessage(R.string.staff_wanted_open_offers) else res.toStaffMessage(R.string.staff_error_save), @@ -284,6 +295,16 @@ class StaffDesiderataViewModel @Inject constructor( } fun consumeMessage() = _state.update { it.copy(message = null) } + + fun consumeOpenCopy() = _state.update { it.copy(openCopy = null) } + + /** The record and copy a received offer produced, when the server names both. */ + private fun receivedCopy(offer: StaffOffer): Pair? { + if (offer.status != "received") return null + val book = offer.receivedBook?.id ?: offer.book?.id ?: return null + val copy = offer.copyId?.takeIf { it > 0 } ?: return null + return book to copy + } } internal fun offerStatusLabel(status: String): Int? = when (status) { @@ -297,7 +318,11 @@ internal fun offerStatusLabel(status: String): Int? = when (status) { /** Donation offers (accept, reject, receive into the catalogue, delete) and the wanted titles. */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun StaffDesiderataScreen(onNavigateUp: () -> Unit, onOpenBook: (Int) -> Unit) { +fun StaffDesiderataScreen( + onNavigateUp: () -> Unit, + onOpenBook: (Int) -> Unit, + onOpenCopy: (bookId: Int, copyId: Int) -> Unit = { book, _ -> onOpenBook(book) }, +) { val vm: StaffDesiderataViewModel = hiltViewModel() val state by vm.state.collectAsStateWithLifecycle() val app: AppViewModel = hiltViewModel() @@ -306,6 +331,8 @@ fun StaffDesiderataScreen(onNavigateUp: () -> Unit, onOpenBook: (Int) -> Unit) { ForbiddenExit(state.forbidden, onNavigateUp) LeaveWhen(!areas.desiderata, onNavigateUp) StaffSnackbarEffect(state.message, snackbar, vm::consumeMessage) + // A receipt made a copy: go straight to it, to give it its code and place. + LaunchedEffect(state.openCopy) { state.openCopy?.let { (book, copy) -> vm.consumeOpenCopy(); onOpenCopy(book, copy) } } Scaffold( topBar = { PinakesTopBar(title = stringResource(R.string.staff_desiderata), onNavigateUp = onNavigateUp) }, @@ -333,7 +360,7 @@ fun StaffDesiderataScreen(onNavigateUp: () -> Unit, onOpenBook: (Int) -> Unit) { title = w.title, coverUrl = w.coverUrl, line1 = w.author, line2 = stringResource(R.string.staff_wanted_open_offers_count, w.openOffers), onClick = { onOpenBook(w.id) }, - trailing = { PinakesTextButton(stringResource(R.string.staff_wanted_mark_received), onClick = { vm.receiveWanted(w) }, enabled = !state.busy) }, + trailing = { PinakesTextButton(stringResource(R.string.staff_wanted_mark_received), onClick = { vm.askReceiveWanted(w) }, enabled = !state.busy) }, modifier = Modifier.padding(vertical = Spacing.xs).testTag("wanted-${w.id}"), ) } @@ -368,6 +395,12 @@ fun StaffDesiderataScreen(onNavigateUp: () -> Unit, onOpenBook: (Int) -> Unit) { } state.selected?.let { offer -> OfferSheet(offer, state.busy, vm, onOpenBook) } + state.confirmReceive?.let { book -> + ConfirmDialog( + title = stringResource(R.string.staff_wanted_receive_confirm_title), body = stringResource(R.string.staff_wanted_receive_confirm_body, book.title), + confirmLabel = stringResource(R.string.staff_wanted_mark_received), onConfirm = vm::receiveWanted, onDismiss = vm::dismissReceiveWanted, + ) + } state.confirmDelete?.let { offer -> ConfirmDialog( title = stringResource(R.string.staff_offer_delete), body = stringResource(R.string.staff_offer_delete_body, offer.title), diff --git a/i18n/de.json b/i18n/de.json index da7c81b..5de45c8 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1210,7 +1210,9 @@ "staff_offer_delete_body": "Das Angebot für „%1$s“ löschen? Das lässt sich nicht rückgängig machen.", "staff_offer_deleted": "Angebot gelöscht", "staff_wanted_open_offers_count": "Offene Angebote: %1$d", - "staff_wanted_mark_received": "Erhalten", + "staff_wanted_mark_received": "Empfangen", + "staff_wanted_receive_confirm_title": "Als erhalten markieren?", + "staff_wanted_receive_confirm_body": "„%1$s“ erhält ein Exemplar im Katalog und verschwindet aus den gesuchten Titeln.", "staff_wanted_received": "„%1$s“ erhalten: Exemplar hinzugefügt", "staff_wanted_open_offers": "Für diesen Titel gibt es offene Angebote: über das Angebot erfassen.", "staff_archives_search": "Titel oder Signatur", diff --git a/i18n/en.json b/i18n/en.json index 24a1131..8ace25f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1210,7 +1210,9 @@ "staff_offer_delete_body": "Delete the offer of “%1$s”? This cannot be undone.", "staff_offer_deleted": "Offer deleted", "staff_wanted_open_offers_count": "Open offers: %1$d", - "staff_wanted_mark_received": "Received", + "staff_wanted_mark_received": "Receive", + "staff_wanted_receive_confirm_title": "Mark as received?", + "staff_wanted_receive_confirm_body": "“%1$s” gets a copy in the catalogue and leaves the wanted titles.", "staff_wanted_received": "“%1$s” received: copy added", "staff_wanted_open_offers": "This title has open offers: receive it from the offer instead.", "staff_archives_search": "Title or reference code", diff --git a/i18n/fr.json b/i18n/fr.json index e7a4498..5cd7c25 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1210,7 +1210,9 @@ "staff_offer_delete_body": "Supprimer la proposition « %1$s » ? C’est irréversible.", "staff_offer_deleted": "Proposition supprimée", "staff_wanted_open_offers_count": "Propositions ouvertes : %1$d", - "staff_wanted_mark_received": "Reçu", + "staff_wanted_mark_received": "Recevoir", + "staff_wanted_receive_confirm_title": "Marquer comme reçu ?", + "staff_wanted_receive_confirm_body": "« %1$s » reçoit un exemplaire dans le catalogue et quitte les titres recherchés.", "staff_wanted_received": "« %1$s » reçu : exemplaire ajouté", "staff_wanted_open_offers": "Ce titre a des propositions ouvertes : recevez-le depuis la proposition.", "staff_archives_search": "Titre ou cote", diff --git a/i18n/it.json b/i18n/it.json index 8c3191f..7090e00 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -1210,7 +1210,9 @@ "staff_offer_delete_body": "Eliminare la proposta per «%1$s»? Non si può annullare.", "staff_offer_deleted": "Proposta eliminata", "staff_wanted_open_offers_count": "Proposte aperte: %1$d", - "staff_wanted_mark_received": "Ricevuto", + "staff_wanted_mark_received": "Ricevi", + "staff_wanted_receive_confirm_title": "Segnare come ricevuto?", + "staff_wanted_receive_confirm_body": "«%1$s» riceve una copia nel catalogo ed esce dai titoli cercati.", "staff_wanted_received": "«%1$s» ricevuto: copia aggiunta", "staff_wanted_open_offers": "Questo titolo ha proposte aperte: ricevilo dalla proposta.", "staff_archives_search": "Titolo o segnatura",