Repository navigation
Receive a wanted title with a clear action, then open its new copy #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"data":{"book_id":77,"copy_id":901,"offer_id":55},"meta":{},"error":null} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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} |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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<Int, Int>? = 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 }, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ) | ||||||
| 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<Int, Int>? { | ||||||
| if (offer.status != "received") return null | ||||||
| val book = offer.receivedBook?.id ?: offer.book?.id ?: return null | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same
Suggested change
|
||||||
| 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), | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The section also renders when only
areas.labelsis on (line 240), but this effect doesn't checkareas.copies. On a server with labels but no per-copy management, the receipt still opensCopyEditorSheet, which is UI the user otherwise can't reach, and saving it callsupdateCopyon an area the server doesn't offer. Gating the auto-open onareas.copieskeeps the jump consistent with the list.