From fa02a5d31fb22f86a065a3352742ddd5332a7f40 Mon Sep 17 00:00:00 2001
From: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Date: Wed, 2 Oct 2024 11:02:13 +0200
Subject: [PATCH] Bug 37334: Allow embed _status for GET
 /biblio/{biblio_id}/items

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
---
 Koha/Item.pm                                  |  50 ++++++
 api/v1/swagger/definitions/item.yaml          |   5 +
 api/v1/swagger/paths/biblios.yaml             |   1 +
 .../tables/items/catalogue_detail.inc         | 147 +++++++++---------
 4 files changed, 131 insertions(+), 72 deletions(-)

diff --git a/Koha/Item.pm b/Koha/Item.pm
index 07fc6ad5c8..f6bc0f6951 100644
--- a/Koha/Item.pm
+++ b/Koha/Item.pm
@@ -1443,6 +1443,56 @@ sub columns_to_str {
     return $values;
 }
 
+sub _status {
+    my ($self) = @_;
+
+    my @statuses;
+    if ( my $checkout = $self->checkout ) {
+        unless ( $checkout->onsite_checkout ) {
+            push @statuses, "checked_out";
+        } else {
+            push @statuses, "local_use";
+        }
+    } elsif ( my $transfer = $self->transfer ) {
+        if ( $transfer->datesent ) {
+            push @statuses, "in_transit";
+        } else {
+            push @statuses, "transit_pending";
+        }
+    }
+    if ( $self->itemlost ) {
+        push @statuses, 'lost';
+    }
+    if ( $self->withdrawn ) {
+        push @statuses, 'withdrawn';
+    }
+    if ( $self->damaged ) {
+        push @statuses, 'damaged';
+    }
+    if ( $self->notforloan || $self->item_type->notforloan  ) {
+        # TODO on a big Koha::Items loop we are going to join with item_type too often, use a cache
+        push @statuses, 'not_for_loan';
+    }
+    if ( $self->first_hold ) {
+        push @statuses, 'on_hold';
+    }
+    if ( C4::Context->preference('UseRecalls') && $self->recall ) {
+        push @statuses, 'recalled';
+    }
+
+    unless (@statuses ){
+        push @statuses, 'available';
+    }
+
+    if ( $self->restricted ) {
+        push @statuses, 'restricted';
+    }
+
+    # TODO in_bundle?
+
+    return join ',', @statuses;
+}
+
 =head3 additional_attributes
 
     my $attributes = $item->additional_attributes;
diff --git a/api/v1/swagger/definitions/item.yaml b/api/v1/swagger/definitions/item.yaml
index b254eb2dc3..cf57ec5f67 100644
--- a/api/v1/swagger/definitions/item.yaml
+++ b/api/v1/swagger/definitions/item.yaml
@@ -322,4 +322,9 @@ properties:
       - object
       - "null"
     description: A return claims object if one exists that's unresolved
+  _status:
+    type:
+      - string
+      - "null"
+    description: The status of the item
 additionalProperties: false
diff --git a/api/v1/swagger/paths/biblios.yaml b/api/v1/swagger/paths/biblios.yaml
index 036179cfa9..ad34c447f1 100644
--- a/api/v1/swagger/paths/biblios.yaml
+++ b/api/v1/swagger/paths/biblios.yaml
@@ -455,6 +455,7 @@
           type: string
           enum:
             - +strings
+            - _status
             - home_library
             - holding_library
             - biblio.title
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc
index 047f095fd5..47110de37c 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc
@@ -227,7 +227,7 @@
     [%# In case or SeparateHoldings we may need to display the number of biblios in each tab %]
     [%# Do we need separate/new endpoints or do we hack the somewhere client-side? %]
     let item_table_url = "/api/v1/biblios/[% biblio.biblionumber | uri %]/items?";
-    let embed = ["+strings,home_library,holding_library,checkout,checkout.patron,transfer,transfer+strings,first_hold,first_hold+strings,first_hold.patron,first_hold.desk"];
+    let embed = ["+strings,_status,home_library,holding_library,checkout,checkout.patron,transfer,transfer+strings,first_hold,first_hold+strings,first_hold.patron,first_hold.desk"];
     [% IF Koha.Preference('LocalCoverImages') %]
         embed.push('cover_image_ids');
     [% END %]
@@ -463,104 +463,107 @@
                 }
             },
             {
-                data: "me.lost_status",
+                data: "me._status",
                 className: "status",
                 searchable: false, // FIXME We are losing the ability to search on the status
                 orderable: false,
                 render: function (data, type, row, meta) {
                     let nodes = "";
-                    if ( row.checkout ) {
-                        [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
-                        row.checkout.patron.library = { name: libraries_names.get(row.checkout.patron.library_id) };
+                    row._status.split(",").forEach( status => {
+                        if ( status == 'checked_out' || status == 'local_use') {
+                            nodes += '<span>';
 
-                        nodes += '<span>';
-                        if ( row.checkout.onsite_checkout ) {
+                            [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
+                            row.checkout.patron.library = { name: libraries_names.get(row.checkout.patron.library_id) };
                             let patron_to_html = $patron_to_html(row.checkout.patron, { url: true, display_cardnumber: true, hide_patron_name });
-                            nodes += _("Currently in local use by %s").format(patron_to_html);
-                        } else {
-                            nodes += '<span class="datedue">';
-                            let patron_to_html = $patron_to_html(row.checkout.patron, { url: true, display_cardnumber: true, hide_patron_name });
-                            nodes += _("Checked out to %s").format(patron_to_html);
+
+                            if ( status == 'local_use' ) {
+                                nodes += _("Currently in local use by %s").format(patron_to_html);
+                            } else {
+                                nodes += '<span class="datedue">';
+                                nodes += _("Checked out to %s").format(patron_to_html);
+                            }
+                            nodes += ': ';
+                            nodes += _("due %s").format($date(row.checkout.due_date, { as_due_date: true }));
+                            nodes += "</span>"
+
                         }
-                        nodes += ': ';
-                        nodes += _("due %s").format($date(row.checkout.due_date, { as_due_date: true }));
-                        nodes += "</span>"
-                    } else if ( row.transfer ) {
-                        if ( row.transfer.datesent ) {
+                        if ( status == 'in_transit' ) {
                             nodes += '<span class="intransit">%s</span>'.format(_("In transit from %s to %s since %s").format(escape_str(row.transfer._strings.from_library.str), escape_str(row.transfer._strings.to_library.str), $date(row.transfer.datesent)));
-                        } else {
+                        }
+
+                        if ( status == 'transit_pending' ) {
                             nodes += '<span class="transitrequested">%s</span>'.format(_("Transit pending from %s to %s since %s").format(escape_str(row.transfer._strings.from_library.str), escape_str(row.transfer._strings.to_library.str), $date(row.transfer.daterequested)));
                         }
-                    }
 
-                    if ( row.lost_status ) {
-                        let lost_lib = av_lost.get(row.lost_status.toString()) || _("Unavailable (lost or missing");
-                        nodes += '<span class="lost">%s</span>'.format(escape_str(lost_lib));
-                        [% IF Koha.Preference('ClaimReturnedLostValue') %]
-                            const hasReturnClaims = row.return_claims.filter(rc => !rc.resolution).length > 0 ? true : false
+                        if ( status == 'lost' ) {
+                            let lost_lib = av_lost.get(row.lost_status.toString()) || _("Unavailable (lost or missing");
+                            nodes += '<span class="lost">%s</span>'.format(escape_str(lost_lib));
+
+                            const hasReturnClaims = row.return_claims && row.return_claims.filter(rc => !rc.resolution).length > 0 ? true : false
                             if(hasReturnClaims) {
                                 nodes += '<span class="claimed_returned">' + _("(Claimed returned)") + '</span>';
                             }
-                        [% END %]
-                    }
+                        }
 
-                    if ( row.withdrawn ) {
-                        let withdrawn_lib = av_withdrawn.get(row.withdrawn.toString()) || _("Withdrawn");
-                        nodes += '<span class="wdn">%s</span>'.format(escape_str(withdrawn_lib));
-                    }
+                        if ( status == 'withdrawn' ) {
+                            let withdrawn_lib = av_withdrawn.get(row.withdrawn.toString()) || _("Withdrawn");
+                            nodes += '<span class="wdn">%s</span>'.format(escape_str(withdrawn_lib));
+                        }
 
-                    if ( row.damaged_status ) {
-                        let damaged_lib = av_damaged.get(row.damaged_status.toString()) || _("Damaged");
-                        nodes += '<span class="dmg">%s</span>'.format(escape_str(damaged_lib));
-                    }
+                        if ( status == 'damaged' ) {
+                            let damaged_lib = av_damaged.get(row.damaged_status.toString()) || _("Damaged");
+                            nodes += '<span class="dmg">%s</span>'.format(escape_str(damaged_lib));
+                        }
 
-                    if ( row.not_for_loan_status || item_types_notforloan.get(row.item_type_id) ) {
-                        let not_for_loan_lib = av_not_for_loan.get(row.not_for_loan_status.toString());
-                        nodes += '<span class="notforloan">%s'.format(_("Not for loan")) + ( not_for_loan_lib ? '<span class="reason"> (%s)</span>'.format(escape_str(not_for_loan_lib)) : '' ) + '</span>';
-                    }
+                        if ( status == 'not_for_loan' ) {
+                            let not_for_loan_lib = av_not_for_loan.get(row.not_for_loan_status.toString());
+                            nodes += '<span class="notforloan">%s'.format(_("Not for loan")) + ( not_for_loan_lib ? '<span class="reason"> (%s)</span>'.format(escape_str(not_for_loan_lib)) : '' ) + '</span>';
+                        }
 
-                    if ( row.first_hold ) {
-                        if ( row.first_hold.waiting_date ) {
-                            if ( row.first_hold.desk ) {
-                                nodes += '<span class="waitingat">%s</span>'.format(_("Waiting at %s, %s since %s.".format(row.first_hold._strings.pickup_library_id.str, row.first_hold.desk.desk_name, $date(row.first_hold.waiting_date))));
+                        if ( status == 'on_hold') {
+                            if ( row.first_hold.waiting_date ) {
+                                if ( row.first_hold.desk ) {
+                                    nodes += '<span class="waitingat">%s</span>'.format(_("Waiting at %s, %s since %s.".format(row.first_hold._strings.pickup_library_id.str, row.first_hold.desk.desk_name, $date(row.first_hold.waiting_date))));
+                                } else {
+                                    nodes += '<span class="waitingat">%s</span>'.format(_("Waiting at %s since %s.".format(row.first_hold._strings.pickup_library_id.str, $date(row.first_hold.waiting_date))));
+                                }
+                                [% IF Koha.Preference('canreservefromotherbranches') %]
+                                    if ( row.first_hold.waiting_date || row.first_hold.priority == 1 ) {
+                                        [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
+                                        row.first_hold.patron.library = { name: libraries_names.get(row.first_hold.patron.library_id) };
+
+                                        let patron_to_html = $patron_to_html(row.first_hold.patron, { url: true, display_cardnumber: true, hide_patron_name });
+                                        nodes += ' <span class="heldfor">%s</span>'.format(_("Hold for: %s").format(patron_to_html));
+                                    }
+                                [% END %]
                             } else {
-                                nodes += '<span class="waitingat">%s</span>'.format(_("Waiting at %s since %s.".format(row.first_hold._strings.pickup_library_id.str, $date(row.first_hold.waiting_date))));
+                                nodes += ' <span class="holdonitem">%s</span>'.format(_("There is an item level hold on this item (priority = %s).").format(row.first_hold.priority));
                             }
-                            [% IF Koha.Preference('canreservefromotherbranches') %]
-                                if ( row.first_hold.waiting_date || row.first_hold.priority == 1 ) {
-                                    [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
-                                    row.first_hold.patron.library = { name: libraries_names.get(row.first_hold.patron.library_id) };
-
-                                    let patron_to_html = $patron_to_html(row.first_hold.patron, { url: true, display_cardnumber: true, hide_patron_name });
-                                    nodes += ' <span class="heldfor">%s</span>'.format(_("Hold for: %s").format(patron_to_html));
-                                }
-                            [% END %]
-                        } else {
-                            nodes += ' <span class="holdonitem">%s</span>'.format(_("There is an item level hold on this item (priority = %s).").format(row.first_hold.priority));
                         }
-                    }
 
-                    [% IF Koha.Preference('UseRecalls') %]
-                        if ( row.recall ) {
-                            if ( row.recall.waiting_date ) {
-                                nodes += '<span>%s</span>'.format(_("Waiting at %s since %s").format(escape_str(row.recall.pickup_library_id.str), $date(row.recall.waiting_date)));
-                            } else {
-                                [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
-                                row.recall.patron.library = { name: libraries_names.get(row.recall.patron.library_id) };
+                        [% IF Koha.Preference('UseRecalls') %]
+                            if ( status == 'recalled' ) {
+                                if ( row.recall.waiting_date ) {
+                                    nodes += '<span>%s</span>'.format(_("Waiting at %s since %s").format(escape_str(row.recall.pickup_library_id.str), $date(row.recall.waiting_date)));
+                                } else {
+                                    [%# Hacky for patron_to_html in case we simply want to display the patron's library name %]
+                                    row.recall.patron.library = { name: libraries_names.get(row.recall.patron.library_id) };
 
-                                let patron_to_html = $patron_to_html(row.recall.patron, {url: true, display_cardnumber: true, hide_patron_name });
-                                nodes += '<span>%s</span>'.format(_("recalled by %s on %s").format(patron_to_html, $date(row.recall.created_date)))
+                                    let patron_to_html = $patron_to_html(row.recall.patron, {url: true, display_cardnumber: true, hide_patron_name });
+                                    nodes += '<span>%s</span>'.format(_("recalled by %s on %s").format(patron_to_html, $date(row.recall.created_date)))
+                                }
                             }
-                        }
-                    [% END %]
+                        [% END %]
 
-                    if ( ! ( row.not_for_loan_status || item_types_notforloan.get(row.item_type_id) || row.checked_out_date || row.lost_status || row.withdrawn || row.damaged_status || row.transfer || row.first_hold || row.recall ) ) {
-                        nodes += ' <span>%s</span>'.format(_("Available"))
-                    }
+                        if ( status == 'available' ) {
+                            nodes += ' <span>%s</span>'.format(_("Available"))
+                        }
 
-                    if ( row.restricted_status ) {
-                        nodes += '<span class="restricted">(%s)</span>'.format(escape_str(av_restricted.get(row.restricted_status.toString())));
-                    }
+                        if ( status == 'restricted') {
+                            nodes += '<span class="restricted">(%s)</span>'.format(escape_str(av_restricted.get(row.restricted_status.toString())));
+                        }
+                    });
 
                     [% IF bundlesEnabled %]
                         if ( row.in_bundle ) {
-- 
2.39.2