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",