diff --git a/api/src/Page/Imaging.php b/api/src/Page/Imaging.php
index 4e2d411ca..147f6dc9a 100644
--- a/api/src/Page/Imaging.php
+++ b/api/src/Page/Imaging.php
@@ -611,12 +611,25 @@ function _get_inspection_images()
array_push($args, $this->arg('sid'));
}
- $images = $this->db->pq("SELECT i.containerid, si.containerinspectionid, ROUND(TIMESTAMPDIFF('HOUR', min(i2.bltimestamp), i.bltimestamp)/24,1) as delta, si.blsampleimageid, si.blsampleid, si.micronsperpixelx, si.micronsperpixely, si.blsampleimagescoreid, si.comments, TO_CHAR(si.bltimestamp, 'DD-MM-YYYY HH24:MI') as bltimestamp, sc.name as scorename, sc.score, sc.colour as scorecolour, max.maxscore, scorecolours.colour as maxscorecolour
+ $order = 'i.bltimestamp';
+
+ if ($this->has_arg('sort_by')) {
+ $cols = array(
+ 'BLTIMESTAMP' => 'i.bltimestamp',
+ 'LOCATION' => 'b.location+0'
+ );
+ $dir = $this->has_arg('order') ? ($this->arg('order') == 'asc' ? 'ASC' : 'DESC') : 'ASC';
+ if (array_key_exists($this->arg('sort_by'), $cols))
+ $order = $cols[$this->arg('sort_by')] . ' ' . $dir;
+ }
+
+ $images = $this->db->pq("SELECT i.containerid, si.containerinspectionid, ROUND(TIMESTAMPDIFF('HOUR', min(i2.bltimestamp), i.bltimestamp)/24,1) as delta, si.blsampleimageid, si.blsampleid, si.micronsperpixelx, si.micronsperpixely, si.blsampleimagescoreid, si.comments, TO_CHAR(si.bltimestamp, 'DD-MM-YYYY HH24:MI') as bltimestamp, sc.name as scorename, sc.score, sc.colour as scorecolour, max.maxscore, scorecolours.colour as maxscorecolour, b.location
FROM blsampleimage si
LEFT OUTER JOIN blsampleimagescore sc ON sc.blsampleimagescoreid = si.blsampleimagescoreid
INNER JOIN containerinspection i ON i.containerinspectionid = si.containerinspectionid
LEFT OUTER JOIN containerinspection i2 ON i.containerid = i2.containerid
+ INNER JOIN blsample b ON b.blsampleid = si.blsampleid
INNER JOIN container c ON c.containerid = i.containerid
INNER JOIN dewar d ON d.dewarid = c.dewarid
INNER JOIN shipping s ON s.shippingid = d.shippingid
@@ -631,7 +644,7 @@ function _get_inspection_images()
WHERE p.proposalid = :1 $where
GROUP BY i.containerid, si.containerinspectionid, i.bltimestamp, si.blsampleimageid, si.blsampleid, si.micronsperpixelx, si.micronsperpixely, si.blsampleimagescoreid, si.comments, TO_CHAR(si.bltimestamp, 'DD-MM-YYYY HH24:MI'), sc.name, sc.score, sc.colour
- ORDER BY i.bltimestamp", $args);
+ ORDER BY $order", $args);
if ($this->has_arg('imid')) {
diff --git a/client/src/css/partials/_base.scss b/client/src/css/partials/_base.scss
index 3167726bd..3fc24eaa0 100644
--- a/client/src/css/partials/_base.scss
+++ b/client/src/css/partials/_base.scss
@@ -21,6 +21,13 @@ body {
}
}
+body.dialog-open {
+ overflow: hidden;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+}
+
a {
color: $link-color;
diff --git a/client/src/css/partials/_utility.scss b/client/src/css/partials/_utility.scss
index c1a9c18d3..eb7e802a0 100644
--- a/client/src/css/partials/_utility.scss
+++ b/client/src/css/partials/_utility.scss
@@ -72,6 +72,10 @@
flex: 0 0 auto
}
+.wrap {
+ flex-wrap: wrap;
+}
+
/* TODO: If we can ever get rid of this class, we can also remove the
work-around in plotly support which needs to circumvent it */
.active {
diff --git a/client/src/js/modules/imaging/views/inspectionimages.js b/client/src/js/modules/imaging/views/inspectionimages.js
new file mode 100644
index 000000000..62ff8217c
--- /dev/null
+++ b/client/src/js/modules/imaging/views/inspectionimages.js
@@ -0,0 +1,94 @@
+define(['views/dialog',
+ 'utils/xhrimage',
+ 'modules/imaging/collections/inspectionimages',
+ ], function(DialogView,
+ XHRImage,
+ InspectionImages) {
+
+ return DialogView.extend({
+ template: '
',
+
+ ui: {
+ images: '.images',
+ },
+
+ initialize: function(options) {
+ // disable scrolling on the main page
+ $('body').addClass('dialog-open')
+ this.inspectionimages = new InspectionImages()
+ this.inspectionimages.queryParams.iid = options.CONTAINERINSPECTIONID
+ this.inspectionimages.queryParams.sort_by = 'LOCATION'
+ },
+
+ inspectionLoaded: function() {
+ var self = this
+ const loadDelay = 200 // milliseconds
+ this.imageTimers = new Map() // Track timers per image
+
+ this.observer = new IntersectionObserver(function(entries, observer) {
+ entries.forEach(entry => {
+ const img = $(entry.target)
+
+ if (entry.isIntersecting) {
+ // Start a delayed load
+ const timerId = setTimeout(() => {
+ let im = new XHRImage()
+ im.load(img.data('src'), function(loadedImage) {
+ img.attr('src', loadedImage.src)
+ })
+ observer.unobserve(entry.target) // Unobserve after loading
+ self.imageTimers.delete(entry.target)
+ }, loadDelay)
+
+ // Store timer so we can cancel if needed
+ self.imageTimers.set(entry.target, timerId)
+ } else {
+ // No longer visible: cancel pending load if exists
+ const timerId = self.imageTimers.get(entry.target)
+ if (timerId) {
+ clearTimeout(timerId)
+ self.imageTimers.delete(entry.target)
+ }
+ }
+ })
+ })
+
+ this.inspectionimages.each(function(inspectionimage) {
+ let locationEl = $('').text(inspectionimage.get('LOCATION'))
+
+ let imageEl = $('
').css('min-height', '400px')
+ .data('src', app.apiurl + '/imaging/inspection/image/' + inspectionimage.get('BLSAMPLEIMAGEID') + '?f=1')
+
+ locationEl.append(imageEl)
+ self.ui.images.append(locationEl)
+ self.observer.observe(imageEl[0])
+ })
+ },
+
+ onRender: function() {
+ this.inspectionimages.fetch().done(this.inspectionLoaded.bind(this))
+ },
+
+ closeDialog: function(e) {
+ DialogView.prototype.closeDialog.apply(this, arguments)
+ this.destroy()
+ },
+
+ onDestroy: function() {
+ // re-enable scrolling on the main page
+ $('body').removeClass('dialog-open')
+ if (this.observer) {
+ this.observer.disconnect()
+ }
+
+ // Cancel all pending image timers
+ if (this.imageTimers) {
+ for (const timer of this.imageTimers.values()) {
+ clearTimeout(timer)
+ }
+ this.imageTimers.clear()
+ }
+ },
+ })
+
+})
diff --git a/client/src/js/modules/shipment/views/containerplate.js b/client/src/js/modules/shipment/views/containerplate.js
index 5a5de4ec5..38e42f612 100644
--- a/client/src/js/modules/shipment/views/containerplate.js
+++ b/client/src/js/modules/shipment/views/containerplate.js
@@ -20,6 +20,7 @@ define(['marionette',
'modules/imaging/views/imagehistory',
'modules/imaging/views/addinspection',
+ 'modules/imaging/views/inspectionimages',
'modules/imaging/views/actualschedule',
'modules/imaging/views/subtosample',
@@ -61,7 +62,7 @@ define(['marionette',
SingleSample,
ContainerInspections, InspectionImage, InspectionImages, ImageViewer, ImageHistoryView,
- AddInspectionView, ActualScheduleView, SubToSampleView,
+ AddInspectionView, InspectionImagesView, ActualScheduleView, SubToSampleView,
ScreenComponentGroups,
ScreenComponents,
@@ -282,6 +283,7 @@ define(['marionette',
'click a.add_inspection': 'showAddInspection',
'click a.view_sched': 'showViewSchedule',
'click @ui.play': 'playInspection',
+ 'click a.inspection_images': 'showInspectionImages',
'dragover @ui.drop': 'dragHover',
'dragleave @ui.drop': 'dragHover',
@@ -452,6 +454,20 @@ define(['marionette',
} else this.ui.ins.val(m.get('CONTAINERINSPECTIONID')).trigger('change')
},
+ showInspectionImages: function(e) {
+ e.preventDefault()
+ this.inspectionImagesView = new InspectionImagesView({ dialog: true, CONTAINERINSPECTIONID: this.ui.ins.val() })
+ app.dialog.show(new DialogView({
+ title: 'Inspection Images',
+ view: this.inspectionImagesView,
+ autoSize: true,
+ destroyOnClose: true,
+ dOptions: {
+ height: $(window).height() * 0.8,
+ },
+ }))
+ },
+
showViewSchedule: function(e) {
e.preventDefault()
diff --git a/client/src/js/templates/imaging/inspectionadd.html b/client/src/js/templates/imaging/inspectionadd.html
index c4e42b946..7b043436c 100644
--- a/client/src/js/templates/imaging/inspectionadd.html
+++ b/client/src/js/templates/imaging/inspectionadd.html
@@ -9,7 +9,7 @@ Add New Container Inspection
-