diff --git a/README.md b/README.md index c08541c3b..0997ef017 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ This file should be copied to create a client/src/js/config.json file and edited | site_name | Site Name to display in footer | | site_link | URL to site home page | | site_image | PNG image of site logo to display in header| +| csv_profile | The csv profile for importing shipments, currently only imca, see src/js/csv/imca.js | +| enable_exp_plan | Whether to enable editing of experimental plan fields when creating samples | +| auto_collect_label | Customise the auto collect label from the default 'Automated' | +| queue_shipment | Allow entire shipment to be queued for automated / mail-in collection | ### Build front end See package.json for the full list of commands that can be run. diff --git a/api/src/Page/Assign.php b/api/src/Page/Assign.php index 608295afe..2834fdf9f 100644 --- a/api/src/Page/Assign.php +++ b/api/src/Page/Assign.php @@ -7,7 +7,7 @@ class Assign extends Page { - public static $arg_list = array('visit' => '\w+\d+-\d+', 'cid' => '\d+', 'did' => '\d+', 'pos' => '\d+', 'bl' => '[\w-]+'); + public static $arg_list = array('visit' => '\w+\d+-\d+', 'cid' => '\d+', 'did' => '\d+', 'pos' => '\d+', 'bl' => '[\w-]+', 'nodup' => '\d'); public static $dispatch = array(array('/visits(/:visit)', 'get', '_blsr_visits'), array('/assign', 'get', '_assign'), @@ -23,54 +23,107 @@ class Assign extends Page # ------------------------------------------------------------------------ # Assign a container function _assign() { - if (!$this->has_arg('visit')) $this->_error('No visit specified'); + if (!$this->has_arg('visit') && !$this->has_arg('prop')) $this->_error('No visit or prop specified'); if (!$this->has_arg('cid')) $this->_error('No container id specified'); if (!$this->has_arg('pos')) $this->_error('No position specified'); - + + $where = 'c.containerid=:1'; + $args = array($this->arg('cid')); + + if ($this->has_arg('visit')) { + $where .= " AND CONCAT(p.proposalcode, p.proposalnumber, '-', bl.visit_number) LIKE :".(sizeof($args)+1); + array_push($args, $this->arg('visit')); + } else { + $where .= " AND CONCAT(p.proposalcode, p.proposalnumber) LIKE :".(sizeof($args)+1); + array_push($args, $this->arg('prop')); + } + $cs = $this->db->pq("SELECT d.dewarid,bl.beamlinename,c.containerid,c.code FROM container c INNER JOIN dewar d ON d.dewarid = c.dewarid INNER JOIN shipping s ON s.shippingid = d.shippingid INNER JOIN blsession bl ON bl.proposalid = s.proposalid INNER JOIN proposal p ON s.proposalid = p.proposalid - WHERE CONCAT(CONCAT(CONCAT(p.proposalcode, p.proposalnumber), '-'), bl.visit_number) LIKE :1 AND c.containerid=:2", array($this->arg('visit'), $this->arg('cid'))); + WHERE $where", $args); if (sizeof($cs) > 0) { $c = $cs[0]; + + $bl = $c['BEAMLINENAME']; + if ($this->staff) { + if ($this->has_arg('bl')) { + $bl = $this->arg('bl'); + } + } + + if ($this->has_arg(('nodup'))) { + $existing = $this->db->pq("SELECT c.containerid, c.name, CONCAT(p.proposalcode, p.proposalnumber) as prop + FROM container c + INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN shipping s ON s.shippingid = d.shippingid + INNER JOIN proposal p ON s.proposalid = s.proposalid + WHERE beamlinelocation=1 AND samplechangerlocation:2", array($bl, $this->arg('pos'))); + + if (sizeof($existing)) { + $ex = $existing[0]; + return $this->_error('A container is already a assigned that position: '+$ex[0]['NAME'] + '('+$ex['PROP']+')'); + } + } + + $this->db->pq("UPDATE dewar SET dewarstatus='processing' WHERE dewarid=:1", array($c['DEWARID'])); - $this->db->pq("UPDATE container SET beamlinelocation=:1,samplechangerlocation=:2,containerstatus='processing' WHERE containerid=:3", array($c['BEAMLINENAME'], $this->arg('pos'), $c['CONTAINERID'])); - $this->db->pq("INSERT INTO containerhistory (containerid,status,location,beamlinename) VALUES (:1,:2,:3,:4)", array($c['CONTAINERID'], 'processing', $this->arg('pos'), $c['BEAMLINENAME'])); - $this->_update_history($c['DEWARID'], 'processing', $c['BEAMLINENAME'], $c['CODE'].' => '.$this->arg('pos')); + $this->db->pq("UPDATE container SET beamlinelocation=:1,samplechangerlocation=:2,containerstatus='processing' WHERE containerid=:3", array($bl, $this->arg('pos'), $c['CONTAINERID'])); + $this->db->pq("INSERT INTO containerhistory (containerid,status,location,beamlinename) VALUES (:1,:2,:3,:4)", array($c['CONTAINERID'], 'processing', $this->arg('pos'), $bl)); + $this->_update_history($c['DEWARID'], 'processing', $bl, $c['CODE'].' => '.$this->arg('pos')); $this->_output(1); + } else { + $this->_error('No such container'); } - - $this->_output(0); } # ------------------------------------------------------------------------ # Unassign a container function _unassign() { - if (!$this->has_arg('visit')) $this->_error('No visit specified'); + if (!$this->has_arg('visit') && !$this->has_arg('prop')) $this->_error('No visit or prop specified'); if (!$this->has_arg('cid')) $this->_error('No container id specified'); - + + $where = 'c.containerid=:1'; + $args = array($this->arg('cid')); + + if ($this->has_arg('visit')) { + $where .= " AND CONCAT(p.proposalcode, p.proposalnumber, '-', bl.visit_number) LIKE :".(sizeof($args)+1); + array_push($args, $this->arg('visit')); + } else { + $where .= " AND CONCAT(p.proposalcode, p.proposalnumber) LIKE :".(sizeof($args)+1); + array_push($args, $this->arg('prop')); + } + $cs = $this->db->pq("SELECT d.dewarid,bl.beamlinename,c.containerid FROM container c INNER JOIN dewar d ON d.dewarid = c.dewarid INNER JOIN shipping s ON s.shippingid = d.shippingid INNER JOIN blsession bl ON bl.proposalid = s.proposalid INNER JOIN proposal p ON s.proposalid = p.proposalid - WHERE CONCAT(CONCAT(CONCAT(p.proposalcode, p.proposalnumber), '-'), bl.visit_number) LIKE :1 AND c.containerid=:2", array($this->arg('visit'), $this->arg('cid'))); + WHERE $where", $args); if (sizeof($cs) > 0) { $c = $cs[0]; + + $bl = $c['BEAMLINENAME']; + if ($this->staff) { + if ($this->has_arg('bl')) { + $bl = $this->arg('bl'); + } + } $this->db->pq("UPDATE container SET samplechangerlocation='',beamlinelocation='',containerstatus='at facility' WHERE containerid=:1",array($c['CONTAINERID'])); - $this->db->pq("INSERT INTO containerhistory (containerid,status,beamlinename) VALUES (:1,:2,:3)", array($c['CONTAINERID'], 'at facility', $c['BEAMLINENAME'])); + $this->db->pq("INSERT INTO containerhistory (containerid,status,beamlinename) VALUES (:1,:2,:3)", array($c['CONTAINERID'], 'at facility', $bl)); //$this->_update_history($c['DEWARID'], 'unprocessing'); $this->_output(1); + } else { + $this->_error('No such container'); } - $this->_output(0); } @@ -102,10 +155,10 @@ function _deactivate() { $this->db->pq("UPDATE container SET containerstatus='at facility', samplechangerlocation='', beamlinelocation='' WHERE containerid=:1", array($c['ID'])); $this->db->pq("INSERT INTO containerhistory (containerid,status) VALUES (:1,:2)", array($c['ID'], 'at facility')); } - $this->_output(1); - + $this->_output(1); + } else { + $this->_error('No such dewar'); } - $this->_output(0); } diff --git a/api/src/Page/Sample.php b/api/src/Page/Sample.php index 712118276..e259e2b7d 100644 --- a/api/src/Page/Sample.php +++ b/api/src/Page/Sample.php @@ -62,6 +62,7 @@ class Sample extends Page 'NAME' => '[\w\s-()]+', 'COMMENTS' => '.*', + 'STAFFCOMMENTS' => '.*', 'SPACEGROUP' => '(\w+)|^$', // Any word character or empty string 'CELL_A' => '\d+(.\d+)?', 'CELL_B' => '\d+(.\d+)?', @@ -117,6 +118,9 @@ class Sample extends Page 'MONOCHROMATOR' => '\w+', 'PRESET' => '\d', 'BEAMLINENAME' => '[\w-]+', + 'AIMEDRESOLUTION' => '\d+(.\d+)?', + 'COLLECTIONMODE' => '\w+', + 'PRIORITY' => '\d+', 'queued' => '\d', 'UNQUEUE' => '\d', @@ -135,7 +139,9 @@ class Sample extends Page 'GROUPORDER' => '\d+', 'TYPE' => '\w+', 'BLSAMPLEGROUPSAMPLEID' => '\d+-\d+', + 'SHIPPINGID' => '\d+', + 'QUEUESTATUS' => '\w+', ); @@ -148,6 +154,8 @@ class Sample extends Page array('/components', 'post', '_add_sample_component'), array('/components/:scid', 'delete', '_remove_sample_component'), + array('/queue/:CONTAINERQUEUESAMPLEID', 'patch', '_update_sample_queue'), + array('/sub(/:ssid)(/sid/:sid)', 'get', '_sub_samples'), array('/sub/:ssid', 'patch', '_update_sub_sample'), array('/sub/:ssid', 'put', '_update_sub_sample_full'), @@ -815,6 +823,12 @@ function _samples() { array_push($args, $this->arg('BLSAMPLEGROUPID')); } + # For a specific shipment + if ($this->has_arg('SHIPPINGID')) { + $where .= ' AND s.shippingid=:'.(sizeof($args)+1); + array_push($args, $this->arg('SHIPPINGID')); + } + # For a specific container if ($this->has_arg('cid')) { $where .= ' AND c.containerid=:'.(sizeof($args)+1); @@ -900,6 +914,7 @@ function _samples() { INNER JOIN proposal p ON p.proposalid = pr.proposalid INNER JOIN container c ON c.containerid = b.containerid INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN shipping s ON s.shippingid = d.shippingid LEFT OUTER JOIN datacollection dc ON b.blsampleid = dc.blsampleid LEFT OUTER JOIN robotaction r ON r.blsampleid = b.blsampleid AND r.actiontype = 'LOAD' $join WHERE $where", $args); @@ -931,9 +946,12 @@ function _samples() { if (array_key_exists($this->arg('sort_by'), $cols)) $order = $cols[$this->arg('sort_by')].' '.$dir; } - $rows = $this->db->paginate("SELECT distinct b.blsampleid, b.crystalid, b.screencomponentgroupid, ssp.blsampleid as parentsampleid, ssp.name as parentsample, b.blsubsampleid, count(distinct si.blsampleimageid) as inspections, CONCAT(p.proposalcode,p.proposalnumber) as prop, b.code, b.location, pr.acronym, pr.proteinid, cr.spacegroup,b.comments,b.name,s.shippingname as shipment,s.shippingid,d.dewarid,d.code as dewar, c.code as container, c.containerid, c.samplechangerlocation as sclocation, count(distinct IF(dc.overlap != 0,dc.datacollectionid,NULL)) as sc, count(distinct IF(dc.overlap = 0 AND dc.axisrange = 0,dc.datacollectionid,NULL)) as gr, count(distinct IF(dc.overlap = 0 AND dc.axisrange > 0,dc.datacollectionid,NULL)) as dc, count(distinct so.screeningid) as ai, count(distinct app.autoprocprogramid) as ap, count(distinct r.robotactionid) as r, round(min(st.rankingresolution),2) as scresolution, max(ssw.completeness) as sccompleteness, round(min(apss.resolutionlimithigh),2) as dcresolution, round(max(apss.completeness),1) as dccompleteness, dp.anomalousscatterer, dp.requiredresolution, cr.cell_a, cr.cell_b, cr.cell_c, cr.cell_alpha, cr.cell_beta, cr.cell_gamma, b.packingfraction, b.dimension1, b.dimension2, b.dimension3, b.shape, cr.theoreticaldensity, cr.name as crystal, pr.name as protein, b.looptype, dp.centringmethod, dp.experimentkind, cq.containerqueueid, TO_CHAR(cq.createdtimestamp, 'DD-MM-YYYY HH24:MI') as queuedtimestamp + $rows = $this->db->paginate("SELECT distinct b.blsampleid, b.crystalid, b.screencomponentgroupid, ssp.blsampleid as parentsampleid, ssp.name as parentsample, b.blsubsampleid, count(distinct si.blsampleimageid) as inspections, CONCAT(p.proposalcode,p.proposalnumber) as prop, b.code, b.location, pr.acronym, pr.proteinid, cr.spacegroup,b.comments, b.staffcomments, b.name,s.shippingname as shipment,s.shippingid,d.dewarid,d.code as dewar, c.code as container, c.containerid, c.samplechangerlocation as sclocation, count(distinct IF(dc.overlap != 0,dc.datacollectionid,NULL)) as sc, count(distinct IF(dc.overlap = 0 AND dc.axisrange = 0,dc.datacollectionid,NULL)) as gr, count(distinct IF(dc.overlap = 0 AND dc.axisrange > 0,dc.datacollectionid,NULL)) as dc, count(distinct so.screeningid) as ai, count(distinct app.autoprocprogramid) as ap, count(distinct r.robotactionid) as r, round(min(st.rankingresolution),2) as scresolution, max(ssw.completeness) as sccompleteness, round(min(apss.resolutionlimithigh),2) as dcresolution, round(max(apss.completeness),1) as dccompleteness, dp.anomalousscatterer, dp.requiredresolution, cr.cell_a, cr.cell_b, cr.cell_c, cr.cell_alpha, cr.cell_beta, cr.cell_gamma, b.packingfraction, b.dimension1, b.dimension2, b.dimension3, b.shape, cr.theoreticaldensity, cr.name as crystal, pr.name as protein, b.looptype, dp.centringmethod, dp.experimentkind, cq.containerqueueid, TO_CHAR(cq.createdtimestamp, 'DD-MM-YYYY HH24:MI') as queuedtimestamp , $cseq $sseq string_agg(cpr.name) as componentnames, string_agg(cpr.density) as componentdensities - ,string_agg(cpr.proteinid) as componentids, string_agg(cpr.acronym) as componentacronyms, string_agg(cpr.global) as componentglobals, string_agg(chc.abundance) as componentamounts, string_agg(ct.symbol) as componenttypesymbols, b.volume, pct.symbol,ROUND(cr.abundance,3) as abundance, TO_CHAR(b.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp, dp.radiationsensitivity, dp.energy, dp.userpath + ,string_agg(cpr.proteinid) as componentids, string_agg(cpr.acronym) as componentacronyms, string_agg(cpr.global) as componentglobals, string_agg(chc.abundance) as componentamounts, string_agg(ct.symbol) as componenttypesymbols, b.volume, pct.symbol,ROUND(cr.abundance,3) as abundance, TO_CHAR(b.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp, dp.radiationsensitivity, dp.energy, dp.userpath, + dp.aimedresolution, dp.preferredbeamsizex, dp.preferredbeamsizey, dp.exposuretime, dp.axisstart, dp.axisrange, dp.numberofimages, dp.transmission, dp.collectionmode, dp.priority, + GROUP_CONCAT(distinct a.spacegroup SEPARATOR ', ') as dcspacegroup, + cqss.status as lastqueuestatus, cq.containerqueueid, cqs.containerqueuesampleid FROM blsample b @@ -951,7 +969,12 @@ function _samples() { INNER JOIN proposal p ON p.proposalid = pr.proposalid LEFT OUTER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL - + LEFT OUTER JOIN containerqueuesample cqs ON cqs.blsampleid = b.blsampleid AND cq.containerqueueid = cqs.containerqueueid + + LEFT OUTER JOIN containerqueuesample cqss ON cqss.containerqueuesampleid = ( + SELECT MAX(containerqueuesampleid) FROM containerqueuesample _cqs WHERE _cqs.blsampleid = b.blsampleid + ) + LEFT OUTER JOIN diffractionplan dp ON dp.diffractionplanid = b.diffractionplanid LEFT OUTER JOIN datacollection dc ON b.blsampleid = dc.blsampleid LEFT OUTER JOIN screening sc ON dc.datacollectionid = sc.datacollectionid @@ -965,6 +988,8 @@ function _samples() { LEFT OUTER JOIN autoprocscaling_has_int aph ON aph.autoprocintegrationid = ap.autoprocintegrationid LEFT OUTER JOIN autoprocscalingstatistics apss ON apss.autoprocscalingid = aph.autoprocscalingid LEFT OUTER JOIN autoprocprogram app ON app.autoprocprogramid = ap.autoprocprogramid AND app.processingstatus = 1 + LEFT OUTER JOIN autoprocscaling aps ON aph.autoprocscalingid = aps.autoprocscalingid + LEFT OUTER JOIN autoproc a ON aps.autoprocid = a.autoprocid LEFT OUTER JOIN blsampleimage si ON b.blsampleid = si.blsampleid @@ -1019,14 +1044,15 @@ function _update_sample_full() { if (!sizeof($samp)) $this->_error('No such sample'); else $samp = $samp[0]; - $this->db->pq("UPDATE blsample set name=:1,comments=:2,code=:3,volume=:4,packingfraction=:5,dimension1=:6,dimension2=:7,dimension3=:8,shape=:9,looptype=:10 WHERE blsampleid=:11", - array($a['NAME'],$a['COMMENTS'],$a['CODE'],$a['VOLUME'],$a['PACKINGFRACTION'],$a['DIMENSION1'],$a['DIMENSION2'],$a['DIMENSION3'],$a['SHAPE'],$a['LOOPTYPE'],$this->arg('sid'))); + $this->db->pq("UPDATE blsample set name=:1,comments=:2,code=:3,volume=:4,packingfraction=:5,dimension1=:6,dimension2=:7,dimension3=:8,shape=:9,looptype=:10,staffcomments=:11 WHERE blsampleid=:12", + array($a['NAME'],$a['COMMENTS'],$a['CODE'],$a['VOLUME'],$a['PACKINGFRACTION'],$a['DIMENSION1'],$a['DIMENSION2'],$a['DIMENSION3'],$a['SHAPE'],$a['LOOPTYPE'],$a['STAFFCOMMENTS'],$this->arg('sid'))); if (array_key_exists('PROTEINID', $a)) { $this->db->pq("UPDATE crystal set spacegroup=:1,proteinid=:2,cell_a=:3,cell_b=:4,cell_c=:5,cell_alpha=:6,cell_beta=:7,cell_gamma=:8,theoreticaldensity=:9 WHERE crystalid=:10", array($a['SPACEGROUP'], $a['PROTEINID'], $a['CELL_A'], $a['CELL_B'], $a['CELL_C'], $a['CELL_ALPHA'], $a['CELL_BETA'], $a['CELL_GAMMA'], $a['THEORETICALDENSITY'], $samp['CRYSTALID'])); - $this->db->pq("UPDATE diffractionplan set anomalousscatterer=:1,requiredresolution=:2, experimentkind=:3, centringmethod=:4, radiationsensitivity=:5, energy=:6, userpath=:7 WHERE diffractionplanid=:8", - array($a['ANOMALOUSSCATTERER'], $a['REQUIREDRESOLUTION'], $a['EXPERIMENTKIND'], $a['CENTRINGMETHOD'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $samp['DIFFRACTIONPLANID'])); + $this->db->pq("UPDATE diffractionplan set anomalousscatterer=:1,requiredresolution=:2, experimentkind=:3, centringmethod=:4, radiationsensitivity=:5, energy=:6, userpath=:7, aimedresolution=:8, preferredbeamsizex=:9, preferredbeamsizey=:10, exposuretime=:11, axisstart=:12, axisrange=:13, numberofimages=:14, transmission=:15, collectionmode=:16, priority=:17 WHERE diffractionplanid=:18", + array($a['ANOMALOUSSCATTERER'], $a['REQUIREDRESOLUTION'], $a['EXPERIMENTKIND'], $a['CENTRINGMETHOD'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $a['AIMEDRESOLUTION'], $a['PREFERREDBEAMSIZEX'], $a['PREFERREDBEAMSIZEY'], $a['EXPOSURETIME'], $a['AXISSTART'], $a['AXISRANGE'], $a['NUMBEROFIMAGES'], $a['TRANSMISSION'], $a['COLLECTIONMODE'], $a['PRIORITY'], + $samp['DIFFRACTIONPLANID'])); } $init_comps = explode(',', $samp['COMPONENTIDS']); @@ -1051,6 +1077,35 @@ function _update_sample_components($initial, $final, $amounts, $crystalid) { } } + // Manually update the status of a sample in the queue + function _update_sample_queue() { + $statuses = array('completed', 'skipped', 'reinspect', 'failed'); + + if (!$this->staff) $this->_error('No access'); + if (!$this->has_arg('prop')) $this->_error('No proposal specified'); + if (!$this->has_arg('CONTAINERQUEUESAMPLEID')) $this->_error('No sample container queue id specified'); + if (!$this->has_arg('QUEUESTATUS') || !in_array($this->arg('QUEUESTATUS'), $statuses)) $this->_error('No status specified'); + + $chk = $this->db->pq("SELECT s.blsampleid + FROM blsample s + INNER JOIN containerqueuesample cqs ON cqs.blsampleid = s.blsampleid + INNER JOIN container c ON c.containerid = s.containerid + INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN shipping sh ON sh.shippingid = d.shippingid + WHERE sh.proposalid=:1 AND cqs.containerqueuesampleid=:2", + array($this->proposalid, $this->arg('CONTAINERQUEUESAMPLEID'))); + + if (!sizeof($chk)) $this->_error('Sample not queued'); + + $this->db->pq('UPDATE containerqueuesample SET endtime=CURRENT_TIMESTAMP, status=:1 WHERE containerqueuesampleid=:2', + array($this->arg('QUEUESTATUS'), $this->arg('CONTAINERQUEUESAMPLEID'))); + + $this->_output(array( + 'CONTAINERQUEUESTATUSID' => $this->arg('CONTAINERQUEUESAMPLEID'), + 'QUEUESTATUS' => $this->arg('QUEUESTATUS') + )); + } + function _add_sample() { if (!$this->has_arg('prop')) $this->_error('No proposal specified'); @@ -1111,12 +1166,12 @@ function _prepare_sample_args($s=null) { if (!$haskey) $this->_error('One or more fields is missing'); - foreach (array('COMMENTS', 'SPACEGROUP', 'CODE', 'ANOMALOUSSCATTERER') as $f) { + foreach (array('COMMENTS', 'STAFFCOMMENTS', 'SPACEGROUP', 'CODE', 'ANOMALOUSSCATTERER', 'COLLECTIONMODE') as $f) { if ($s) $a[$f] = array_key_exists($f, $s) ? $s[$f] : ''; else $a[$f] = $this->has_arg($f) ? $this->arg($f) : ''; } - foreach (array('CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'SCREENCOMPONENTGROUPID', 'BLSUBSAMPLEID', 'COMPONENTIDS', 'COMPONENTAMOUNTS', 'REQUIREDRESOLUTION', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'THEORETICALDENSITY', 'LOOPTYPE', 'ENERGY', 'USERPATH') as $f) { + foreach (array('CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'SCREENCOMPONENTGROUPID', 'BLSUBSAMPLEID', 'COMPONENTIDS', 'COMPONENTAMOUNTS', 'REQUIREDRESOLUTION', 'AIMEDRESOLUTION', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'THEORETICALDENSITY', 'LOOPTYPE', 'ENERGY', 'USERPATH', 'PRIORITY', 'PREFERREDBEAMSIZEX', 'PREFERREDBEAMSIZEY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION') as $f) { if ($s) $a[$f] = array_key_exists($f, $s) ? $s[$f] : null; else $a[$f] = $this->has_arg($f) ? $this->arg($f) : null; } @@ -1126,8 +1181,8 @@ function _prepare_sample_args($s=null) { function _do_add_sample($a) { - $this->db->pq("INSERT INTO diffractionplan (diffractionplanid, requiredresolution, anomalousscatterer, centringmethod, experimentkind, radiationsensitivity, energy, userpath) VALUES (s_diffractionplan.nextval, :1, :2, :3, :4, :5, :6, :7) RETURNING diffractionplanid INTO :id", - array($a['REQUIREDRESOLUTION'], $a['ANOMALOUSSCATTERER'], $a['CENTRINGMETHOD'], $a['EXPERIMENTKIND'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'])); + $this->db->pq("INSERT INTO diffractionplan (diffractionplanid, requiredresolution, anomalousscatterer, centringmethod, experimentkind, radiationsensitivity, energy, userpath, aimedresolution, preferredbeamsizex, preferredbeamsizey, exposuretime, axisstart, axisrange, numberofimages, transmission, collectionmode, priority) VALUES (s_diffractionplan.nextval, :1, :2, :3, :4, :5, :6, :7, :8, :9, :10, :11, :12, :13, :14, :15, :16, :17) RETURNING diffractionplanid INTO :id", + array($a['REQUIREDRESOLUTION'], $a['ANOMALOUSSCATTERER'], $a['CENTRINGMETHOD'], $a['EXPERIMENTKIND'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $a['AIMEDRESOLUTION'], $a['PREFERREDBEAMSIZEX'], $a['PREFERREDBEAMSIZEY'], $a['EXPOSURETIME'], $a['AXISSTART'], $a['AXISRANGE'], $a['NUMBEROFIMAGES'], $a['TRANSMISSION'], $a['COLLECTIONMODE'], $a['PRIORITY'])); $did = $this->db->id(); if (!array_key_exists('CRYSTALID', $a)) { @@ -1393,7 +1448,7 @@ function _update_sample() { if (!sizeof($samp)) $this->_error('No such sample'); else $samp = $samp[0]; - $sfields = array('CODE', 'NAME', 'COMMENTS', 'VOLUME', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'POSITION', 'CONTAINERID', 'LOOPTYPE'); + $sfields = array('CODE', 'NAME', 'COMMENTS', 'STAFFCOMMENTS', 'VOLUME', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'POSITION', 'CONTAINERID', 'LOOPTYPE'); foreach ($sfields as $f) { if ($this->has_arg($f)) { $this->db->pq("UPDATE blsample SET $f=:1 WHERE blsampleid=:2", array($this->arg($f), $samp['BLSAMPLEID'])); @@ -1415,7 +1470,7 @@ function _update_sample() { } } - $dfields = array('REQUIREDRESOLUTION', 'ANOMALOUSSCATTERER', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'ENERGY', 'USERPATH'); + $dfields = array('REQUIREDRESOLUTION', 'ANOMALOUSSCATTERER', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'ENERGY', 'USERPATH', 'AIMEDRESOLUTION', 'PREFERREDBEAMSIZEX', 'PREFERREDBEAMSIZEY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'COLLECTIONMODE', 'PRIORITY'); foreach ($dfields as $f) { if ($this->has_arg($f)) { $this->db->pq("UPDATE diffractionplan SET $f=:1 WHERE diffractionplanid=:2", array($this->arg($f), $samp['DIFFRACTIONPLANID'])); diff --git a/api/src/Page/Shipment.php b/api/src/Page/Shipment.php index a2de872fe..9c2062a92 100644 --- a/api/src/Page/Shipment.php +++ b/api/src/Page/Shipment.php @@ -86,6 +86,7 @@ class Shipment extends Page 'unassigned' => '[\w-]+', // Container fields + 'REGISTRY' => '([\w-])+', 'DEWARID' => '\d+', 'CAPACITY' => '\d+', 'CONTAINERTYPE' => '\w+', @@ -122,6 +123,8 @@ class Shipment extends Page 'manifest' => '\d', 'currentuser' => '\d', + 'PROPOSALCODE' => '\w+', + 'CONTAINERQUEUEID' => '\d+' ); @@ -129,6 +132,7 @@ class Shipment extends Page array('/shipments', 'post', '_add_shipment'), array('/shipments/:sid', 'patch', '_update_shipment'), array('/send/:sid', 'get', '_send_shipment'), + array('/return/:sid', 'get', '_return_shipment'), array('/countries', 'get', '_get_countries'), @@ -164,6 +168,7 @@ class Shipment extends Page array('/containers/:cid', 'patch', '_update_container'), array('/containers/move', 'get', '_move_container'), array('/containers/queue', 'get', '_queue_container'), + array('/containers/queue/:CONTAINERQUEUEID', 'post', '_update_container_queue'), array('/containers/barcode/:BARCODE', 'get', '_check_container'), @@ -1207,6 +1212,41 @@ function _send_shipment() { $this->_output(1); } + + function _return_shipment() { + if (!$this->has_arg('prop')) $this->_error('No proposal specified'); + if (!$this->has_arg('sid')) $this->_error('No shipping id specified'); + + $ship = $this->db->pq("SELECT s.shippingid + FROM shipping s + INNER JOIN proposal p ON s.proposalid = p.proposalid + WHERE p.proposalid = :1 AND s.shippingid = :2", array($this->proposalid,$this->arg('sid'))); + + if (!sizeof($ship)) $this->_error('No such shipment'); + $ship = $ship[0]; + + $containers = $this->db->pq("SELECT c.containerid + FROM container c + INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL + WHERE d.shippingid = :1", array($ship['SHIPPINGID'])); + if (sizeof($containers)) $this->_error('Cannot return shipment, there are still uncompleted queued containers: View'); + + $this->db->pq("UPDATE shipping SET shippingstatus='returned' where shippingid=:1", array($ship['SHIPPINGID'])); + $this->db->pq("UPDATE dewar SET dewarstatus='returned' where shippingid=:1", array($ship['SHIPPINGID'])); + + $dewars = $this->db->pq("SELECT d.dewarid, s.visit_number as vn, s.beamlinename as bl, TO_CHAR(s.startdate, 'DD-MM-YYYY HH24:MI') as startdate + FROM dewar d + LEFT OUTER JOIN blsession s ON s.sessionid = d.firstexperimentid + WHERE d.shippingid=:1", array($ship['SHIPPINGID'])); + foreach ($dewars as $d) { + $this->db->pq("INSERT INTO dewartransporthistory (dewartransporthistoryid,dewarid,dewarstatus,arrivaldate) + VALUES (s_dewartransporthistory.nextval,:1,'returned',CURRENT_TIMESTAMP)", + array($d['DEWARID'])); + } + + $this->_output(1); + } # Show and accept terms to use diamonds shipping account @@ -1302,11 +1342,20 @@ function _get_all_containers() { } } + if ($this->has_arg('PROPOSALCODE')) { + $where .= " AND p.proposalcode LIKE :".(sizeof($args)+1); + array_push($args, $this->arg('PROPOSALCODE')); + } if ($this->has_arg('PUCK')) { $where .= " AND c.containertype LIKE 'Puck'"; } + # For a specific shipment + if ($this->has_arg('SHIPPINGID')) { + $where .= ' AND sh.shippingid=:'.(sizeof($args)+1); + array_push($args, $this->arg('SHIPPINGID')); + } if ($this->has_arg('did')) { $where .= ' AND d.dewarid=:'.(sizeof($args)+1); @@ -1356,6 +1405,11 @@ function _get_all_containers() { array_push($args, $this->arg('CONTAINERREGISTRYID')); } + if ($this->has_arg('REGISTRY')) { + $where .= ' AND reg.barcode = :'.(sizeof($args)+1); + array_push($args, $this->arg('REGISTRY')); + } + if ($this->has_arg('currentuser')) { $where .= ' AND c.ownerid = :'.(sizeof($args)+1); array_push($args, $this->user->personid); @@ -1373,6 +1427,7 @@ function _get_all_containers() { LEFT OUTER JOIN containerinspection ci ON ci.containerid = c.containerid AND ci.state = 'Completed' LEFT OUTER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL LEFT OUTER JOIN containerqueue cq2 ON cq2.containerid = c.containerid AND cq2.completedtimestamp IS NOT NULL + LEFT OUTER JOIN containerregistry reg ON reg.containerregistryid = c.containerregistryid $join WHERE $where $having", $args); @@ -1396,7 +1451,7 @@ function _get_all_containers() { array_push($args, $start); array_push($args, $end); - $order = 'c.bltimestamp DESC'; + $order = 'c.containerid DESC'; if ($this->has_arg('ty')) { if ($this->arg('ty') == 'todispose') { @@ -1411,7 +1466,8 @@ function _get_all_containers() { if ($this->has_arg('sort_by')) { $cols = array('NAME' => 'c.code', 'DEWAR' => 'd.code', 'SHIPMENT' => 'sh.shippingname', 'SAMPLES' => 'count(s.blsampleid)', 'SHIPPINGID' =>'sh.shippingid', 'LASTINSPECTION' => 'max(ci.bltimestamp)', 'INSPECTIONS' => 'count(ci.containerinspectionid)', 'DCCOUNT' => 'COUNT(distinct dc.datacollectionid)', 'SUBSAMPLES' => 'count(distinct ss.blsubsampleid)', - 'LASTQUEUECOMPLETED' => 'max(cq2.completedtimestamp)', 'QUEUEDTIMESTAMP' => 'max(cq.createdtimestamp)' + 'LASTQUEUECOMPLETED' => 'max(cq2.completedtimestamp)', 'QUEUEDTIMESTAMP' => 'max(cq.createdtimestamp)', + 'BLTIMESTAMP' => 'c.bltimestamp' ); $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; @@ -1422,7 +1478,9 @@ function _get_all_containers() { ses3.beamlinename as firstexperimentbeamline, pp.name as pipeline, TO_CHAR(max(cq2.completedtimestamp), 'HH24:MI DD-MM-YYYY') as lastqueuecompleted, TIMESTAMPDIFF('MINUTE', max(cq2.completedtimestamp), max(cq2.createdtimestamp)) as lastqueuedwell, - c.ownerid, CONCAT(pe.givenname, ' ', pe.familyname) as owner + c.ownerid, CONCAT(pe.givenname, ' ', pe.familyname) as owner, + CONCAT(SUM(IF(dp.collectionmode = 'auto', 1, 0)), 'A, ', SUM(IF(dp.collectionmode = 'manual', 1, 0)), 'M') as modes, + lc.cardname FROM container c INNER JOIN dewar d ON d.dewarid = c.dewarid LEFT OUTER JOIN blsession ses3 ON d.firstexperimentid = ses3.sessionid @@ -1445,6 +1503,9 @@ function _get_all_containers() { LEFT OUTER JOIN blsession ses ON c.sessionid = ses.sessionid LEFT OUTER JOIN processingpipeline pp ON c.prioritypipelineid = pp.processingpipelineid LEFT OUTER JOIN person pe ON c.ownerid = pe.personid + LEFT OUTER JOIN diffractionplan dp ON dp.diffractionplanid = s.diffractionplanid + + LEFT OUTER JOIN labcontact lc ON sh.sendinglabcontactid = lc.labcontactid $join WHERE $where @@ -1495,7 +1556,15 @@ function _queue_container() { $cqid = $chkq[0]['CONTAINERQUEUEID']; - $this->db->pq("UPDATE containerqueuesample SET containerqueueid = NULL WHERE containerqueueid=:1", array($cqid)); + // For pucks delete the containerqueuesample items + if (stripos($chkc[0]['CONTAINERTYPE'], 'puck') !== false) { + $this->db->pq("DELETE FROM containerqueuesample WHERE containerqueueid=:1", array($cqid)); + + // For plates we have a pre queued "sample is ready to queue", where containerqueueid is null + // so just unset containerqueueid in containerqueuesample + } else { + $this->db->pq("UPDATE containerqueuesample SET containerqueueid = NULL WHERE containerqueueid=:1", array($cqid)); + } $this->db->pq("DELETE FROM containerqueue WHERE containerqueueid=:1", array($cqid)); $this->_output(); @@ -1506,23 +1575,60 @@ function _queue_container() { $this->db->pq("INSERT INTO containerqueue (containerid, personid) VALUES (:1, :2)", array($this->arg('CONTAINERID'), $this->user->personid)); $qid = $this->db->id(); - $samples = $this->db->pq("SELECT ss.blsubsampleid, cqs.containerqueuesampleid FROM blsubsample ss - INNER JOIN blsample s ON s.blsampleid = ss.blsampleid - INNER JOIN container c ON c.containerid = s.containerid - INNER JOIN dewar d ON d.dewarid = c.dewarid - INNER JOIN shipping sh ON sh.shippingid = d.shippingid - INNER JOIN proposal p ON p.proposalid = sh.proposalid - INNER JOIN containerqueuesample cqs ON cqs.blsubsampleid = ss.blsubsampleid - WHERE p.proposalid=:1 AND c.containerid=:2 AND cqs.containerqueueid IS NULL", array($this->proposalid, $this->arg('CONTAINERID'))); - - foreach ($samples as $s) { - $this->db->pq("UPDATE containerqueuesample SET containerqueueid=:1 WHERE containerqueuesampleid=:2", array($qid, $s['CONTAINERQUEUESAMPLEID'])); + // For pucks samples are queued + if (stripos($chkc[0]['CONTAINERTYPE'], 'puck') !== false) { + $this->_queue_samples($this->arg('CONTAINERID'), $qid); + + // For plates subsamples are queued + } else { + $subsamples = $this->db->pq("SELECT ss.blsubsampleid, cqs.containerqueuesampleid FROM blsubsample ss + INNER JOIN blsample s ON s.blsampleid = ss.blsampleid + INNER JOIN container c ON c.containerid = s.containerid + INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN shipping sh ON sh.shippingid = d.shippingid + INNER JOIN proposal p ON p.proposalid = sh.proposalid + INNER JOIN containerqueuesample cqs ON cqs.blsubsampleid = ss.blsubsampleid + WHERE p.proposalid=:1 AND c.containerid=:2 AND cqs.containerqueueid IS NULL", array($this->proposalid, $this->arg('CONTAINERID'))); + + foreach ($subsamples as $s) { + $this->db->pq("UPDATE containerqueuesample SET containerqueueid=:1 WHERE containerqueuesampleid=:2", array($qid, $s['CONTAINERQUEUESAMPLEID'])); + } } $this->_output(array('CONTAINERQUEUEID' => $qid)); } } + function _queue_samples($cid, $qid) { + $samples = $this->db->pq("SELECT s.blsampleid + FROM blsample s + INNER JOIN container c ON c.containerid = s.containerid + INNER JOIN dewar d ON d.dewarid = c.dewarid + INNER JOIN shipping sh ON sh.shippingid = d.shippingid + INNER JOIN proposal p ON p.proposalid = sh.proposalid + WHERE p.proposalid=:1 AND c.containerid=:2", + array($this->proposalid, $cid)); + + foreach ($samples as $s) { + $this->db->pq("INSERT INTO containerqueuesample (blsampleid, containerqueueid) VALUES (:1, :2)", array($s['BLSAMPLEID'], $qid)); + } + } + + # Manually update a container queue status to completed + function _update_container_queue() { + if (!$this->staff) $this->_error("No access"); + + $cq = $this->db->pq("SELECT containerqueueid + FROM containerqueue WHERE containerqueueid=:1", + array($this->arg('CONTAINERQUEUEID'))); + + if (!sizeof($cq)) $this->_error("No such container queue"); + + $this->db->pq("UPDATE containerqueue SET completedtimestamp=CURRENT_TIMESTAMP WHERE containerqueueid=:1", + array($this->arg('CONTAINERQUEUEID'))); + + $this->_output(1); + } # Move Container function _move_container() { @@ -1611,6 +1717,11 @@ function _add_container() { if ($this->has_arg('AUTOMATED')) { $this->db->pq("INSERT INTO containerqueue (containerid, personid) VALUES (:1, :2)", array($cid, $this->user->personid)); + $qid = $this->db->id(); + + if (stripos($this->arg('CONTAINERTYPE'), 'puck') !== false) { + $this->_queue_samples($cid, $qid); + } } $this->_output(array('CONTAINERID' => $cid)); @@ -1701,10 +1812,11 @@ function _container_registry() { } - $tot = $this->db->pq("SELECT count(r.containerregistryid) as tot + $tot = $this->db->pq("SELECT count(distinct r.containerregistryid) as tot FROM containerregistry r LEFT OUTER JOIN containerregistry_has_proposal rhp on rhp.containerregistryid = r.containerregistryid LEFT OUTER JOIN proposal p ON p.proposalid = rhp.proposalid + LEFT OUTER JOIN container c ON c.containerregistryid = r.containerregistryid WHERE $where", $args); $tot = intval($tot[0]['TOT']); @@ -1730,7 +1842,7 @@ function _container_registry() { } $rows = $this->db->paginate("SELECT r.containerregistryid, r.barcode, GROUP_CONCAT(distinct CONCAT(p.proposalcode,p.proposalnumber) SEPARATOR ', ') as proposals, count(distinct c.containerid) as instances, TO_CHAR(r.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp, - TO_CHAR(max(c.bltimestamp),'DD-MM-YYYY') as lastuse, max(CONCAT(p.proposalcode,p.proposalnumber)) as prop, r.comments, COUNT(distinct cr.containerreportid) as reports + TO_CHAR(max(c.bltimestamp),'DD-MM-YYYY') as lastuse, max(CONCAT(p.proposalcode,p.proposalnumber)) as prop, r.comments, COUNT(distinct cr.containerreportid) as reports, c.code as lastname FROM containerregistry r LEFT OUTER JOIN containerregistry_has_proposal rhp on rhp.containerregistryid = r.containerregistryid LEFT OUTER JOIN proposal p ON p.proposalid = rhp.proposalid diff --git a/client/package.json b/client/package.json index c10891dd6..abe93a86f 100644 --- a/client/package.json +++ b/client/package.json @@ -67,6 +67,8 @@ "jquery.flot.tooltip": "^0.9.0", "luxon": "^1.25.0", "markdown": "^0.5.0", + "moment": "^2.24.0", + "papaparse": "^5.2.0", "plotly.js": "^1.52.2", "promise": "^8.0.3", "three": "^0.83.0", diff --git a/client/src/css/partials/_assign.scss b/client/src/css/partials/_assign.scss index d75075225..144d91556 100644 --- a/client/src/css/partials/_assign.scss +++ b/client/src/css/partials/_assign.scss @@ -13,6 +13,13 @@ background: $content-sub-header-background; float: left; + input { + width: calc(99% - 10px); + padding: 10px 5px; + border-radius: 5px; + border: none; + } + @media (max-width: $breakpoint-small) { @include cols(4,0.5%,0.2%); } diff --git a/client/src/css/partials/_content.scss b/client/src/css/partials/_content.scss index 457052a54..1de1142a1 100644 --- a/client/src/css/partials/_content.scss +++ b/client/src/css/partials/_content.scss @@ -1176,6 +1176,39 @@ ul.status { background-color: #87ceeb; } + + // Queue statuses + &.skipped { + &:before { + content: 'Skipped' + } + + background-color: #fdfd96; + } + + &.reinspect { + &:before { + content: 'Re-inspect' + } + + background-color: #ffb347; + } + + &.completed { + &:before { + content: 'Completed' + } + + background-color: #77dd77; + } + + &.failed { + &:before { + content: 'Failed' + } + + background-color: #ff6961; + } } } @@ -1991,3 +2024,19 @@ ul.messages { } } + + +.dropimage { + color: $content-search-background; + padding: 20px; + border: 2px dashed $content-search-background; + margin: 2% 0; + text-align: center; + border-radius: 5px; + + &.active { + color: $content-header-color; + background: $content-dark-background; + text-decoration: italic; + } +} \ No newline at end of file diff --git a/client/src/css/partials/_tables.scss b/client/src/css/partials/_tables.scss index 04e951596..278f53fc7 100644 --- a/client/src/css/partials/_tables.scss +++ b/client/src/css/partials/_tables.scss @@ -17,7 +17,7 @@ padding: 5px; } - td.extra, th.extra, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto { + td.extra, th.extra, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto, th.dp, td.dp { display: none; &.show { @@ -26,7 +26,7 @@ } @media (max-width: $breakpoint-vsmall) { - td.extra, th.extra,, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto { + td.extra, th.extra,, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto, th.dp, td.dp { &.show { display: block; } diff --git a/client/src/js/csv/imca.js b/client/src/js/csv/imca.js new file mode 100644 index 000000000..45cd28ac8 --- /dev/null +++ b/client/src/js/csv/imca.js @@ -0,0 +1,92 @@ +define([], function() { + + return { + // The csv column names + headers: ['Puck', 'Pin', 'Project', 'Priority', 'Mode', 'Notes to Staff', 'Collection strategy', 'Contact person', 'Expected space group', 'Expected Cell Dimensions', 'Expected Resolution', 'Minimum Resolution Required to Collect', 'Recipe', 'Exposure time', 'Image Width', 'Phi', 'Attenuation', 'Aperture', 'Detector Distance', 'Prefix for frames', 'Observed Resolution', 'Comments From Staff', 'Status'], + + // ... and their ISPyB table mapping + mapping: ['CONTAINER', 'LOCATION', 'ACRONYM', 'PRIORITY', 'COLLECTIONMODE', 'COMMENTS', 'COMMENTS', 'OWNER', 'SPACEGROUP', 'CELL', 'AIMEDRESOLUTION', 'REQUIREDRESOLUTION', 'RECIPE', 'EXPOSURETIME', 'AXISRANGE', 'AXISROTATION', 'TRANSMISSION', 'PREFERREDBEAMSIZEX', 'DETECTORDISTANCE', 'PREFIX', 'DCRESOLUTION', 'STAFFCOMMENTS', 'STATUS'], + + // Columns to show on the import page + columns: { + LOCATION: 'Location', + PROTEINID: 'Protein', + NAME: 'Sample', + PRIORITY: 'Priority', + COLLECTIONMODE: 'Mode', + COMMENTS: 'Comments', + SPACEGROUP: 'Spacegroup', + CELL: 'Cell', + AIMEDRESOLUTION: 'Aimed Res', + REQUIREDRESOLUTION: 'Required Res', + EXPOSURETIME: 'Exposure (s)', + AXISRANGE: 'Axis Osc', + NUMBEROFIMAGES: 'No. Images', + TRANSMISSION: 'Transmission', + PREFERREDBEAMSIZEX: 'Beamsize', + }, + + // Import transforms + transforms: { + CELL: function(v, m) { + var comps = v.split(/\s+/) + _.each(['CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA'], function(ax, i) { + if (comps.length > i) m[ax] = comps[i].replace(',', '') + }) + }, + AXISROTATION: function(v, m) { + if (m.AXISRANGE) m.NUMBEROFIMAGES = m.AXISROTATION / m.AXISRANGE + }, + SPACEGROUP: function(v, m) { + m.SPACEGROUP = v.replace(/[\(\)]/g, '') + }, + LOCATION: function(v, m) { + if (!this.xcount) this.xcount = 1 + m.NAME = 'x'+(this.xcount++) + }, + COLLECTIONMODE: function(v, m) { + m.COLLECTIONMODE = v.toLowerCase() + } + }, + + // Export transforms + export: { + CELL: function(m) { + return `${m.CELL_A}, ${m.CELL_B}, ${m.CELL_C}, ${m.CELL_ALPHA}, ${m.CELL_BETA}, ${m.CELL_GAMMA}`.trim() + }, + + STATUS: function(m) { + var status = 'skipped' + if (m.QUEUEDTIMESTAMP) status = 'queued'; + if (m.R > 0) status = 'recieved' + if (m.DC > 0) status = 'collected' + + return status + }, + + AXISROTATION: function(m) { + return m.AXISRANGE * m.NUMBEROFIMAGES + }, + + COMMENTS: function(m, h) { + var comments = m.COMMENTS.split(' | ') + return comments.length > 1 && h == 'Collection strategy' ? comments[1] : comments[0] + } + }, + + exampleCSV: `Puck,Pin,Project,Priority,Mode,Notes to Staff,Collection strategy,Contact person,Expected space group,Expected Cell Dimensions,Expected Resolution,Minimum Resolution Required to Collect,Recipe,Exposure time,Image Width,Phi,Attenuation,Aperture,Detector Distance,Prefix for frames,Observed Resolution,Comments From Staff,Status +Blue53,1,a,1,Manual,Tricky,Do best you can,Luke,C2,"143.734, 67.095, 76.899, 90, 110.45, 90",1.9-3.5,4,luke-360.rcp,,,,,,,,,, +Blue53,2,a,1,Manual,Very tricky,New crystals,Luke,C2,140 65 75 90 110 90,1.8-2.4,3.5,,0.1,0.25,,95,5,250,image_,,, +Blue53,2,a,1,Manual,Very tricky,New crystals,Luke,C2,140 65 75 90 110 90,1.8-2.4,3.5,,0.1,0.25,,95,5,250,image_,,, +Blue53,3,b,3,Auto,Routine,SeMet,Luke,P2,52.4 39.8 65.0 108.5,1.5,1.7,,0.04,0.25,360,,10,300,,,, +Blue53,4,c,3,Auto,Rods,Native,Luke,P21,39 69.2 60 90 105.3,1.5,1.7,,0.04,0.25,360,95,20,,image_,,, +Blue53,5,d,8,,Plates,,Luke,C222,280 45 112 102 90,1.5,1.7,,0.04,0.25,360,95,50,300,image_,,, +Blue54,1,e,,Auto,,,,P212121,67 82 276,2.1,2.5,,,0.25,180,,10,350,image_,,, +Blue54,2,e,4,,,,Luke,P2(1)2(1)2(1),67 82 276,,1.7,luke-180.rcp,,,,,,,,,, +Blue54,3,f,,Auto,,,,P222,,2.1,,,0.04,,180,95,,350,image_,,, +Blue54,4,g,4,Auto,,,Luke,,,2.1,2.5,,0.04,0.25,180,75,,350,image_,,, +Blue54,5,h,99,Auto,,,Luke,P222,,2.2,2.5,,0.04,0.25,180,95,,400,image_,,, + ` + } + +}) diff --git a/client/src/js/models/sample.js b/client/src/js/models/sample.js index a15c694bd..77058a223 100644 --- a/client/src/js/models/sample.js +++ b/client/src/js/models/sample.js @@ -60,6 +60,15 @@ define(['backbone', 'collections/components', DIMENSION2: '', DIMENSION3: '', SHAPE: '', + AIMEDRESOLUTION: '', + COLLECTIONMODE: '', + PRIORITY: '', + EXPOSURETIME: '', + AXISSTART: '', + AXISRANGE: '', + NUMBEROFIMAGES: '', + TRANSMISSION: '', + PREFERREDBEAMSIZEX: '', }, validation: { @@ -157,6 +166,51 @@ define(['backbone', 'collections/components', maxLength: 40, }, + AIMEDRESOLUTION: { + required: false, + pattern: 'number', + }, + + COLLECTIONMODE: { + required: false, + pattern: 'word', + }, + + PRIORITY: { + required: false, + pattern: 'number', + }, + + EXPOSURETIME: { + required: false, + pattern: 'number', + }, + + AXISSTART: { + required: false, + pattern: 'number', + }, + + AXISRANGE: { + required: false, + pattern: 'number', + }, + + NUMBEROFIMAGES: { + required: false, + pattern: 'number', + }, + + TRANSMISSION: { + required: false, + pattern: 'number', + }, + + PREFERREDBEAMSIZEX: { + required: false, + pattern: 'number', + }, + COMPONENTAMOUNTS: function(from_ui, attr, all_values) { var values = all_values.components.pluck('ABUNDANCE') diff --git a/client/src/js/modules/assign/controller.js b/client/src/js/modules/assign/controller.js index a5a5e6a9f..4a97ac323 100644 --- a/client/src/js/modules/assign/controller.js +++ b/client/src/js/modules/assign/controller.js @@ -4,7 +4,8 @@ define(['marionette', 'modules/assign/views/selectvisit', 'modules/assign/views/assign', - ], function(Marionette, Visit, Visits, SelectVisitView, AssignView) { + 'modules/assign/views/scanassign', + ], function(Marionette, Visit, Visits, SelectVisitView, AssignView, ScanAssignView) { var bc = { title: 'Assign Containers', url: '/assign' } @@ -16,7 +17,7 @@ define(['marionette', var visits = new Visits(null, { queryParams: { next: 1 } }) visits.fetch({ success: function() { - app.bc.reset([bc]), + app.bc.reset([bc]) app.content.show(new SelectVisitView({ collection: visits })) }, error: function() { @@ -45,7 +46,18 @@ define(['marionette', app.message({ title: 'No such visit', message: 'The specified visit doesnt exist' }) } }) - } + }, + + // A simple assign page by scanning barcodes + scanAssign: function(bl) { + if (!app.staff) { + app.message({ title: 'No access', message: 'You do not have access to that page' }) + return + } + + app.bc.reset([{ title: 'Assign Containers' }, { title: 'Barcode Scan'}, { title: bl }]) + app.content.show(new ScanAssignView({ bl: bl })) + }, } app.addInitializer(function() { diff --git a/client/src/js/modules/assign/router.js b/client/src/js/modules/assign/router.js index 4b61df45e..d7576902f 100644 --- a/client/src/js/modules/assign/router.js +++ b/client/src/js/modules/assign/router.js @@ -6,6 +6,7 @@ define(['marionette', 'modules/assign/controller'], function(Marionette, c) { appRoutes: { 'assign': 'selectVisit', 'assign/visit/:visit(/page/:page)': 'assignVisit', + 'assign/scan/:bl': 'scanAssign', }, loadEvents: ['assign:visit'], diff --git a/client/src/js/modules/assign/views/scanassign.js b/client/src/js/modules/assign/views/scanassign.js new file mode 100644 index 000000000..02b8f0641 --- /dev/null +++ b/client/src/js/modules/assign/views/scanassign.js @@ -0,0 +1,257 @@ +define(['marionette', 'backbone', + 'views/pages', + 'collections/containers', + 'modules/assign/collections/pucknames', + 'modules/shipment/models/containerregistry', + 'utils', + 'templates/assign/scanassign.html', + 'backbone-validation' + ], function(Marionette, + Backbone, + Pages, + Containers, + PuckNames, + ContainerRegistry, + utils, + template) { + + var ValidatedContainerRegistry = ContainerRegistry.extend({}) + _.extend(ValidatedContainerRegistry.prototype, Backbone.Validation.mixin); + + var ContainerView = Marionette.CompositeView.extend({ + template: _.template(' View Container

<%-PROP%>: <%-NAME%>

'), + className: 'container assigned', + + events: { + click: 'unassignContainer' + }, + + // Unassign Containers + unassignContainer: function(e, options) { + if ($(e.target).is('a') || $(e.target).is('i')) return; + + console.log('this.beal', this.getOption('bl')) + utils.confirm({ + title: 'Confirm Container Unassignment', + content: 'Are you sure you want to unassign "'+this.model.get('NAME')+'" from sample changer position "'+this.model.get('SAMPLECHANGERLOCATION')+'"?', + callback: this.doUnAssign.bind(this, options) + }) + }, + + doUnAssign: function() { + Backbone.ajax({ + url: app.apiurl+'/assign/unassign', + data: { + nodup: 1, + prop: this.model.get('PROP'), cid: this.model.get('CONTAINERID'), bl: this.getOption('bl') + }, + success: this.unassignUpdateGUI.bind(this), + error: function() { + app.alert({ message: 'Something went wrong unassigning this container' }) + + }, + }) + }, + + unassignUpdateGUI: function() { + this.model.set({ SAMPLECHANGERLOCATION: null }) + } + + }) + + + // Sample Changer Positions + var PositionView = Marionette.CompositeView.extend({ + template: _.template('<%-id%>
'), + className: 'bl_puck', + + childView: ContainerView, + childViewContainer: '.ac', + childViewOptions: function() { + return { + bl: this.getOption('bl') + } + }, + + ui: { + name: '.name', + barcode: 'input[name=barcode]', + }, + + events: { + click: 'focusInput', + 'change @ui.barcode': 'findContainer', + 'keyup @ui.barcode': 'findContainer', + }, + + collectionEvents: { + 'change reset': 'render', + }, + + focusInput: function() { + this.ui.barcode.focus() + }, + + findContainer: function() { + if (this.ui.barcode.val() && this.validate()) { + this.containers.fetch().done(this.assignContainer.bind(this)) + } + }, + + validate: function() { + var error = this.registryModel.preValidate('BARCODE', this.ui.barcode.val()) + + if (error) this.ui.barcode.addClass('ferror').removeClass('fvalid') + else this.ui.barcode.removeClass('ferror').addClass('fvalid') + + return error ? false : true + }, + + assignContainer: function() { + if (this.containers.length) { + var container = this.containers.at(0) + + utils.confirm({ + title: 'Confirm Assign Container', + content: 'Barcode matched "'+container.get('PROP')+': '+container.get('NAME')+'" from dewar "'+container.get('DEWAR')+'" with owner "'+container.get('OWNER')+'". Do you want to assign this to sample changer position "'+this.model.get('id')+'"?', + callback: this.doAssignContainer.bind(this) + }) + } else { + app.alert({ message: 'No containers found for barcode: '+this.ui.barcode.val() }) + } + + }, + + doAssignContainer: function() { + var container = this.containers.at(0) + Backbone.ajax({ + url: app.apiurl+'/assign/assign', + data: { + prop: container.get('PROP'), + cid: container.get('CONTAINERID'), + pos: this.model.get('id'), + bl: this.getOption('bl') + }, + success: this.assignUpdateGUI.bind(this), + error: function() { + app.alert({ message: 'Something went wrong assigning this container' }) + }, + }) + }, + + assignUpdateGUI: function() { + var container = this.containers.at(0) + container.set({ SAMPLECHANGERLOCATION: this.model.get('id').toString() }) + this.assigned.add(container) + }, + + getBarcode: function() { + return this.ui.barcode.val() + }, + + initialize: function(options) { + this.collection = new Containers() + this.assigned = options.assigned + this.listenTo(this.assigned, 'change:SAMPLECHANGERLOCATION change sync add remove', this.updateCollection, this) + this.updateCollection() + + this.listenTo(this.getOption('pucknames'), 'sync', this.getNameModel) + + this.findContainer = _.debounce(this.findContainer.bind(this), 500) + + this.containers = new Containers() + this.containers.queryParams.all = 1 + this.containers.queryParams.REGISTRY = this.getBarcode.bind(this) + + this.registryModel = new ValidatedContainerRegistry() + }, + + getNameModel: function() { + this.name = this.getOption('pucknames').findWhere({ id: this.model.get('id') }) + if (this.name) { + this.listenTo(this.name, 'change update', this.updateName) + this.updateName() + } + }, + + updateName: function() { + if (this.name && this.name.get('name')) this.ui.name.text(' - '+this.name.get('name')) + }, + + updateCollection: function() { + this.collection.reset(this.assigned.findWhere({ SAMPLECHANGERLOCATION: this.model.get('id').toString() })) + }, + + onRender: function() { + this.updateName() + + if (this.collection.length > 0) { + this.ui.barcode.hide() + } + }, + + + }) + + + var SampleChangerView = Marionette.CollectionView.extend({ + className: 'clearfix', + childView: PositionView, + childViewOptions: function() { + return { + assigned: this.getOption('assigned'), + pucknames: this.getOption('pucknames'), + bl: this.getOption('bl'), + } + } + }) + + + + return Marionette.LayoutView.extend({ + template: template, + className: 'content', + + regions: { + rassigned: '.rassigned' + }, + + templateHelpers: function() { + return { + bl: this.getOption('bl'), + } + }, + + refresh: function() { + this.assigned.fetch() + }, + + initialize: function() { + this.assigned = new Containers(null, { queryParams: { assigned: 1, bl: this.getOption('bl'), all: 1 }, state: { pageSize: 9999 } }) + this.assigned.fetch() + + this.pucknames = new PuckNames() + this.pucknames.state.pageSize = 100 + this.pucknames.queryParams.bl = this.getOption('bl') + this.pucknames.fetch() + }, + + onShow: function() { + var pucks = this.getOption('bl') in app.config.pucks ? app.config.pucks[this.getOption('bl')] : 10 + + var positions = new Backbone.Collection(_.map(_.range(1,pucks+1), function(i) { return { id: i } })) + this.rassigned.show(new SampleChangerView({ + collection: positions, + assigned: this.assigned, + pucknames: this.pucknames, + bl: this.getOption('bl') + })) + + }, + + onDestroy: function() { + this.pucknames.stop() + }, + }) + +}) diff --git a/client/src/js/modules/samples/views/view.js b/client/src/js/modules/samples/views/view.js index bd0031f56..796d32d3e 100644 --- a/client/src/js/modules/samples/views/view.js +++ b/client/src/js/modules/samples/views/view.js @@ -53,8 +53,16 @@ define(['marionette', ui: { comp: 'input[name=COMPONENTID]', }, + + templateHelpers: function() { + return { + AUTO_LABEL: this.automated_label + } + }, initialize: function(options) { + this.automated_label = app.config.auto_collect_label || 'Automated' + Backbone.Validation.bind(this); this.dcs = new DCCol(null, { queryParams: { sid: this.model.get('BLSAMPLEID'), pp: 5 } }) diff --git a/client/src/js/modules/shipment/controller.js b/client/src/js/modules/shipment/controller.js index dcd1b5f92..b53fe3047 100644 --- a/client/src/js/modules/shipment/controller.js +++ b/client/src/js/modules/shipment/controller.js @@ -7,6 +7,7 @@ define(['backbone', 'modules/shipment/views/shipments', 'modules/shipment/views/shipment', 'modules/shipment/views/shipmentadd', + 'modules/shipment/views/fromcsv', 'models/container', 'collections/containers', @@ -16,6 +17,9 @@ define(['backbone', 'modules/shipment/views/containers', 'modules/imaging/views/queuecontainer', + 'modules/shipment/views/queuedcontainers', + 'modules/shipment/views/containerreview', + 'modules/shipment/models/containerregistry', 'modules/shipment/collections/containerregistry', 'modules/shipment/views/containerregistry', @@ -46,9 +50,10 @@ define(['backbone', ], function(Backbone, GetView, - Dewar, Shipment, Shipments, - ShipmentsView, ShipmentView, ShipmentAddView, - Container, Containers, ContainerView, ContainerPlateView, /*ContainerAddView,*/ ContainersView, QueueContainerView, + Dewar, Shipment, Shipments, + ShipmentsView, ShipmentView, ShipmentAddView, ImportFromCSV, + Container, Containers, ContainerView, ContainerPlateView, /*ContainerAddView,*/ ContainersView, QueueContainerView, + QueuedContainers, ReviewContainer, ContainerRegistry, ContainersRegistry, ContainerRegistryView, RegisteredContainer, RegisteredDewar, DewarRegistry, DewarRegView, RegDewarView, RegDewarAddView, DewarRegistryView, DispatchView, TransferView, Dewars, DewarOverview, ManifestView, DewarStats, CreateAWBView, RebookPickupView, @@ -116,6 +121,37 @@ define(['backbone', } }, + // Import csv based on selected profile + import_csv: function(sid) { + if (!app.config.csv_profile) { + app.message({ title: 'CSV Import Not Enabled', message: 'Shipment CSV import is not currently enabled'}) + return + } + + var lookup = new ProposalLookup({ field: 'SHIPPINGID', value: sid }) + lookup.find({ + success: function() { + var shipment = new Shipment({ SHIPPINGID: sid }) + shipment.fetch({ + success: function() { + app.bc.reset([bc, { title: shipment.get('SHIPPINGNAME') }, { title: 'Import from CSV' }]) + app.content.show(new ImportFromCSV({ model: shipment, format: 'imca' })) + }, + error: function() { + app.bc.reset([bc]) + app.message({ title: 'No such shipment', message: 'The specified shipment could not be found'}) + }, + }) + }, + + error: function() { + app.bc.reset([bc, { title: 'No such shipment' }]) + app.message({ title: 'No such shipment', message: 'The specified shipment could not be found'}) + } + }) + }, + + create_awb: function(sid) { var shipment = new Shipment({ SHIPPINGID: sid }) shipment.fetch({ @@ -325,6 +361,40 @@ define(['backbone', }) }, + queued_containers: function(s, ty, pt, bl, sid, page) { + if (!app.staff) { + app.message({ title: 'No access', message: 'You do not have access to that page'}) + return + } + + app.bc.reset([bc, { title: 'Queued Containers' }]) + app.content.show(new QueuedContainers({ params: { s: s, ty: ty, pt: pt, bl: bl, sid: sid, page: page }})) + }, + + container_review: function(cid) { + var lookup = new ProposalLookup({ field: 'CONTAINERID', value: cid }) + lookup.find({ + success: function() { + var container = new Container({ CONTAINERID: cid }) + container.fetch({ + success: function() { + app.bc.reset([bc, { title: container.get('SHIPMENT'), url: '/shipments/sid/'+container.get('SHIPPINGID') }, { title: 'Containers' }, { title: 'Review' }, { title: container.get('NAME') }]) + app.content.show(new ReviewContainer({ model: container })) + }, + error: function() { + app.bc.reset([bc, { title: 'No such container' }]) + app.message({ title: 'No such container', message: 'The specified container could not be found'}) + }, + }) + }, + + error: function() { + app.bc.reset([bc, { title: 'No such container' }]) + app.message({ title: 'No such container', message: 'The specified container could not be found'}) + } + }) + }, + dewar_registry: function(ty, s, page) { app.loading() @@ -478,6 +548,11 @@ define(['backbone', controller.view_container(cid, iid, sid) }) + app.on('container:review', function(cid) { + app.navigate('containers/review/'+cid) + controller.container_review(cid) + }) + app.on('rdewar:show', function(fc) { app.navigate('dewars/registry/'+fc) controller.view_dewar(fc) diff --git a/client/src/js/modules/shipment/router.js b/client/src/js/modules/shipment/router.js index 99fc5c71c..3009b7db0 100644 --- a/client/src/js/modules/shipment/router.js +++ b/client/src/js/modules/shipment/router.js @@ -9,6 +9,8 @@ define(['utils/lazyrouter'], function(LazyRouter) { 'shipments/awb/sid/:sid': 'create_awb', 'shipments/pickup/sid/:sid': 'rebook_pickup', + 'shipments/csv/:sid': 'import_csv', + 'containers/cid/:cid(/iid/:iid)(/sid/:sid)': 'view_container', 'containers/queue/:cid': 'queue_container', 'containers/add/did/:did': 'add_container', @@ -17,6 +19,9 @@ define(['utils/lazyrouter'], function(LazyRouter) { 'containers/registry(/ty/:ty)(/s/:s)(/page/:page)': 'container_registry', 'containers/registry/:crid': 'view_rcontainer', + 'containers/queued(/s/:s)(/ty/:ty)(/pt/:pt)(/bl/:bl)(/sid/:sid)(/page/:page)': 'queued_containers', + 'containers/review/:cid': 'container_review', + 'dewars(/s/:s)(/page/:page)': 'dewar_list', 'dewars/dispatch/:did': 'dispatch_dewar', 'dewars/transfer/:did': 'transfer_dewar', @@ -32,7 +37,7 @@ define(['utils/lazyrouter'], function(LazyRouter) { 'migrate': 'migrate', }, - loadEvents: ['shipments:show', 'shipment:show', 'rcontainer:show', 'rdewar:show'], + loadEvents: ['shipments:show', 'shipment:show', 'rcontainer:show', 'rdewar:show', 'container:review'], loadModule: function(loadedCallback) { import(/* webpackChunkName: "shipping" */ 'modules/shipment/controller').then(controller => { diff --git a/client/src/js/modules/shipment/views/container.js b/client/src/js/modules/shipment/views/container.js index 7ca2843ef..78d350a78 100644 --- a/client/src/js/modules/shipment/views/container.js +++ b/client/src/js/modules/shipment/views/container.js @@ -58,10 +58,12 @@ define(['marionette', ext: '.extrainfo', auto: '.auto', extrastate: '.extra-state', + dpstate: '.dp-state', }, events: { 'click @ui.ext': 'toggleExtra', + 'click a.dpinfo': 'toggleDP', 'click a.queue': 'confirmQueueContainer', 'click a.unqueue': 'confirmUnqueueContainer', }, @@ -69,6 +71,8 @@ define(['marionette', templateHelpers: function() { return { IS_STAFF: app.staff, + ENABLE_EXP_PLAN: app.config.enable_exp_plan, + AUTO_LABEL: this.automated_label } }, @@ -79,11 +83,20 @@ define(['marionette', : this.ui.extrastate.addClass('fa-plus').removeClass('fa-minus') }, + toggleDP: function(e) { + e.preventDefault() + this.table.currentView.toggleDP() + this.table.currentView.dpState() ? this.ui.dpstate.addClass('fa-minus').removeClass('fa-plus') + : this.ui.dpstate.addClass('fa-plus').removeClass('fa-minus') + }, + createSamples: function() { this.samples = new Samples(null, { state: { pageSize: 9999 } }) }, initialize: function(options) { + this.automated_label = app.config.auto_collect_label || 'Automated' + var self = this this.createSamples() this.samples.queryParams.cid = options.model.get('CONTAINERID') @@ -160,10 +173,10 @@ define(['marionette', updateAutoCollection: function() { if (this.model.get('CONTAINERQUEUEID')) { - this.ui.auto.html('This container was queued for auto collection on '+this.model.escape('QUEUEDTIMESTAMP')) + this.ui.auto.html('This container was queued for '+this.automated_label.toLowerCase()+' collection on '+this.model.escape('QUEUEDTIMESTAMP')) this.ui.auto.append(' Unqueue') } else { - this.ui.auto.html(' Queue this container for Auto Collect') + this.ui.auto.html(' Queue this container for '+this.automated_label.toLowerCase()+' collection') } }, @@ -171,7 +184,7 @@ define(['marionette', e.preventDefault() utils.confirm({ title: 'Queue Container?', - content: 'Are you sure you want to queue this container for auto collection?', + content: 'Are you sure you want to queue this container for '+this.automated_label.toLowerCase()+' collection?', callback: this.doQueueContainer.bind(this) }) }, diff --git a/client/src/js/modules/shipment/views/containeradd.js b/client/src/js/modules/shipment/views/containeradd.js index e26afe8d5..b19ede558 100644 --- a/client/src/js/modules/shipment/views/containeradd.js +++ b/client/src/js/modules/shipment/views/containeradd.js @@ -95,6 +95,8 @@ define(['backbone', SHIPPINGID: this.dewar.get('SHIPPINGID'), SHIPMENT: this.dewar.get('SHIPPINGNAME'), DEWAR: this.dewar.get('CODE'), + ENABLE_EXP_PLAN: app.config.enable_exp_plan, + AUTO_LABEL: this.automated_label } }, @@ -114,6 +116,7 @@ define(['backbone', autoprocessing_pipeline: 'select[name=PIPELINE]', auto: 'input[name=AUTOMATED]', extrastate: '.extra-state', + dp: '.dp-state', }, @@ -134,6 +137,7 @@ define(['backbone', 'change @ui.type': 'setType', 'click @ui.ext': 'toggleExtra', + 'click a.dpinfo': 'toggleDP', 'keypress .ui-combobox input': 'excelNavigate', 'keypress input.sname': 'excelNavigate', @@ -283,6 +287,13 @@ define(['backbone', } }, + toggleDP: function(e) { + e.preventDefault() + this.table.currentView.toggleDP() + this.table.currentView.dpState() ? this.ui.dpstate.addClass('fa-minus').removeClass('fa-plus') + : this.ui.dpstate.addClass('fa-plus').removeClass('fa-minus') + }, + isForImager: function() { return !(!this.ui.imager.val()) }, @@ -517,6 +528,8 @@ define(['backbone', }, initialize: function(options) { + this.automated_label = app.config.auto_collect_label || 'Automated' + this.ready = [] this.dewar = options.dewar diff --git a/client/src/js/modules/shipment/views/containerreview.js b/client/src/js/modules/shipment/views/containerreview.js new file mode 100644 index 000000000..f5a65bcbe --- /dev/null +++ b/client/src/js/modules/shipment/views/containerreview.js @@ -0,0 +1,157 @@ +define(['backbone', + 'marionette', + 'backgrid', + 'views/table', + 'utils/table', + 'collections/samples', + 'templates/shipment/containerreview.html' +], function(Backbone, + Marionette, + Backgrid, + TableView, + table, + Samples, + template) { + + var QueueStatusCell = Backgrid.Cell.extend({ + render: function() { + var st = this.model.escape('LASTQUEUESTATUS') + if (st) this.$el.html('') + + return this + } + }) + + var UCTemplate = '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
ABCαβγ
<%-CELL_A%><%-CELL_B%><%-CELL_C%><%-CELL_ALPHA%><%-CELL_BETA%><%-CELL_GAMMA%>
' + + var ActionCell = Backgrid.Cell.extend({ + className: 'nowrap', + + events: { + 'click a.reinspect': 'markReinspect', + 'click a.skip': 'markSkip', + 'click a.completed': 'markCompleted' + }, + + render: function() { + if (app.staff && this.model.get('CONTAINERQUEUESAMPLEID')) { + var cs = this.model.get('LASTQUEUESTATUS') + if (cs != 'reinspect') this.$el.html('') + if (cs != 'skipped') this.$el.append(' ') + if (cs != 'completed') this.$el.append(' ') + } + + this.$el.append('   View Sample') + + return this + }, + + markReinspect: function(e) { + e.preventDefault() + this.doMarkSample('reinspect') + }, + + markSkip: function(e) { + e.preventDefault() + this.doMarkSample('skipped') + }, + + markCompleted: function(e) { + e.preventDefault() + this.doMarkSample('completed') + }, + + doMarkSample: function(status) { + var self = this + Backbone.ajax({ + url: app.apiurl+'/sample/queue/'+this.model.get('CONTAINERQUEUESAMPLEID'), + data: JSON.stringify({ + prop: app.prop, + QUEUESTATUS: status + }), + type: 'PATCH', + success: function() { + app.alert({ className: 'message notify', message: 'Sample queue status upated', scrollTo: false }) + self.model.collection.fetch() + }, + error: function() { + app.alert({ message: 'Something went wrong updating this samples queue status, please try again' }) + }, + + }) + }, + }) + + return Marionette.LayoutView.extend({ + className: 'content', + template: template, + + regions: { + rsamples: '.rsamples' + }, + + initialize: function(options) { + this.samples = new Samples(null, { state: { pageSize: 9999 } }) + this.samples.queryParams.cid = options.model.get('CONTAINERID') + this.samples.fetch() + }, + + onRender: function() { + var columns = [ + { name: 'LOCATION', label: 'Location', cell: 'string', editable: false }, + { name: 'NAME', label: 'Name', cell: 'string', editable: false }, + { name: 'ACRONYM', label: 'Protein', cell: 'string', editable: false }, + { name: 'COMMENTS', label: 'Comment', cell: 'string', editable: false }, + { name: 'SPACEGROUP', label: 'SG', cell: 'string', editable: false }, + { label: 'Unit Cell', cell: table.TemplateCell, template: UCTemplate, editable: false }, + { name: 'REQUIREDRESOLUTION', label: 'Required Res', cell: 'string', editable: false }, + { name: 'AIMEDRESOLUTION', label: 'Aimed Res', cell: 'string', editable: false }, + { name: 'COLLECTIONMODE', label: 'Mode', cell: 'string', editable: false }, + { name: 'PRIORITY', label: 'Priority', cell: 'string', editable: false }, + { name: 'EXPOSURETIME', label: 'Exposure (s)', cell: 'string', editable: false }, + { name: 'AXISRANGE', label: 'Axis Range', cell: 'string', editable: false }, + { name: 'AXISSTART', label: 'No Images', cell: 'string', editable: false }, + { name: 'TRASMISSION', label: 'Transmission', cell: 'string', editable: false }, + { name: 'DCRESOLUTION', label: 'Observed Res', cell: 'string', editable: false }, + { name: 'DCSPACEGROUP', label: 'Observed SG', cell: 'string', editable: false }, + { name: 'STAFFCOMMENTS', label: 'Staff Comments', cell: 'string', editable: app.staff }, + { label: 'Status', cell: table.StatusCell, editable: false }, + { label: 'Queue', cell: QueueStatusCell, editable: false }, + { label: '', cell: ActionCell, editable: false }, + ] + + this.rsamples.show(new TableView({ + collection: this.samples, + columns: columns + })) + + this.listenTo(this.samples, 'change:STAFFCOMMENTS', this.saveStaffComment, this) + }, + + saveStaffComment: function(m, v) { + m.save(m.changedAttributes(), { patch: true }) + }, + }) +}) diff --git a/client/src/js/modules/shipment/views/fromcsv.js b/client/src/js/modules/shipment/views/fromcsv.js new file mode 100644 index 000000000..9a46fcc08 --- /dev/null +++ b/client/src/js/modules/shipment/views/fromcsv.js @@ -0,0 +1,545 @@ +define(['backbone', + 'marionette', + 'papaparse', + 'models/protein', + 'collections/proteins', + 'models/sample', + 'collections/samples', + 'models/container', + 'collections/containers', + 'collections/dewars', + 'views/validatedrow', + 'views/form', + 'modules/shipment/collections/platetypes', + 'modules/shipment/collections/containerregistry', + 'collections/users', + 'modules/shipment/collections/distinctproteins', + + 'utils/sgs', + 'utils/collectionmode', + + 'templates/shipment/fromcsv.html', + 'templates/shipment/fromcsvtable.html', + 'templates/shipment/fromcsvcontainer.html' + ], function( + Backbone, + Marionette, + Papa, + Protein, + Proteins, + Sample, + Samples, + Container, + Containers, + Dewars, + ValidatedRow, + FormView, + PlateTypes, + ContainerRegistry, + Users, + DistinctProteins, + SG, + COLM, + template, table, container) { + + + var GridRow = ValidatedRow.extend({ + template: false, + tagName: 'tr', + + columnTypes: { + LOCATION: function(v) { + return ''+v+'' + }, + + CELL: function(v, model) { + return ` + + + + + + + + ` + }, + COLLECTIONMODE: function() { + return '' + }, + PROTEINID: function(v, m) { + var newProtein = v == 0 ? 'active' : '' + return ''+m.escape('ACRONYM')+'' + }, + SPACEGROUP: function(v, m) { + return '' + } + }, + + onRender: function() { + var cts = this.getOption('columnTypes') + var columns = _.map(this.getOption('profile').columns, function(c, k) { + var val = this.model.get(k) || '' + return cts[k] ? cts[k](val, this.model, this) : '' + }, this) + + this.$el.html(columns.join('')) + this.$el.find('select[name=SPACEGROUP]').val(this.model.get('SPACEGROUP')) + this.$el.find('select[name=COLLECTIONMODE]').val(this.model.get('COLLECTIONMODE')) + this.model.validate() + }, + }) + + var TableView = Marionette.CompositeView.extend({ + tagName: "table", + className: 'samples reflow', + template: table, + childView: GridRow, + childViewOptions: function() { + return { + profile: this.getOption('profile'), + } + }, + + ui: { + tr: 'thead tr', + }, + + onRender: function() { + var headers = _.map(this.getOption('profile').columns, function(c, k) { + return ''+c+'' + }) + + this.ui.tr.html(headers.join('')) + }, + + }) + + var ContainerView = Marionette.LayoutView.extend({ + template: _.template('

<%-NAME%>

'), + + regions: { + rsamples: '.rsamples', + rcontainer: '.rcontainer', + }, + + initialize: function(options) { + this.samples = new Samples() + this.listenTo(options.samples, 'sync reset', this.generateSamples) + this.generateSamples() + }, + + generateSamples: function() { + this.samples.reset(this.getOption('samples').where({ CONTAINER: this.model.get('NAME') })) + }, + + onRender: function() { + this.rsamples.show(new TableView({ + collection: this.samples, + profile: this.getOption('profile'), + })) + this.rcontainer.show(new ModifyContainerView({ + model: this.model, + platetypes: this.getOption('platetypes'), + users: this.getOption('users'), + containerregistry: this.getOption('containerregistry'), + })) + } + }) + + var ModifyContainerView = FormView.extend({ + template: container, + ui: { + containertype: '[name=CONTAINERTYPE]', + registry: '[name=CONTAINERREGISTRYID]', + person: '[name=PERSONID]', + }, + + events: { + 'change select': 'updateModel', + 'change input': 'updateModel', + }, + + createModel: function() { + + }, + + updateModel: function(e) { + var attr = $(e.target).attr('name') + console.log('updateModel', attr, e.target.value) + if (attr == 'CONTAINERTYPE') { + this.updateContainerType() + } else { + this.model.set({ [attr]: e.target.value }) + } + }, + + updateContainerType: function() { + var containerType = this.getOption('platetypes').findWhere({ name: this.ui.containertype.val() }) + this.model.set({ CONTAINERTYPE: this.ui.containertype.val(), CAPACITY: containerType.get('capacity') }) + }, + + onRender: function() { + this.ui.containertype.html(this.getOption('platetypes').opts()) + this.ui.registry.html(''+this.getOption('containerregistry').opts({ empty: true })) + this.ui.person.html(this.getOption('users').opts()).val(this.model.get('OWNERID') || app.personid) + + if (!this.model.get('CONTAINERTYPE')) { + this.updateContainerType() + } + + if (!this.model.get('CONTAINERREGISTRYID')) { + var reg = this.getOption('containerregistry') + var nearest = reg.findWhere({ LASTNAME: this.model.get('NAME') }) + this.model.set({ CONTAINERREGISTRYID: nearest ? nearest.get('CONTAINERREGISTRYID') : '!' }) + } + this.ui.registry.val(this.model.get('CONTAINERREGISTRYID')) + + this.model.isValid(true) + } + }) + + var ContainersView = Marionette.CollectionView.extend({ + childView: ContainerView, + childViewOptions: function() { + return { + samples: this.getOption('samples'), + profile: this.getOption('profile'), + platetypes: this.getOption('platetypes'), + containerregistry: this.getOption('containerregistry'), + users: this.getOption('users'), + proteins: this.getOption('proteins'), + } + } + }) + + + var MessageView = Marionette.ItemView.extend({ + tagName: 'li', + template: _.template('<%-message%>') + }) + + var MessagesView = Marionette.CollectionView.extend({ + tagName: 'ul', + childView: MessageView + }) + + var Message = Backbone.Model.extend({ + + }) + + return Marionette.LayoutView.extend({ + template: template, + className: 'content', + + regions: { + rcontainers: '.rcontainers', + rmessages: '.rmessages', + }, + + ui: { + drop: '.dropimage', + pnew: '.pnew', + }, + + events: { + 'dragover @ui.drop': 'dragHover', + 'dragleave @ui.drop': 'dragHover', + 'drop @ui.drop': 'uploadFile', + 'click .submit': 'import', + 'click a.export': 'export', + }, + + addMessage: function(options) { + this.messages.add(new Message({ message: options.message })) + }, + + import: function(e) { + e.preventDefault() + this.messages.reset() + + if (!this.containers.length && !this.samples.length) { + app.alert({ message: 'No containers and samples found' }) + return + } + + var valid = true + this.containers.each(function(c) { + var cValid = c.isValid(true) + console.log(c.get('CODE'), c) + if (!cValid) { + valid = false + this.addMessage({ message: `Container ${c.get('NAME')} is invalid` }) + } + }, this) + + this.samples.each(function(s) { + var sValid = s.isValid(true) + console.log(s.get('NAME'), s) + if (!sValid) { + valid = false + this.addMessage({ message: `Sample ${s.get('NAME')} is invalid` }) + } + }, this) + + var pos = this.$el.find('.top').offset().top + $('html, body').animate({scrollTop: pos}, 300); + + if (!valid) { + app.alert({ message: 'Shipment is not valid' }) + return + } + + this.messages.reset() + + var self = this + var pp = [] + this.newProteins.each(function(p) { + pp.push(p.save({}, { + success: function() { + self.addMessage({ message: 'Created component: '+p.get('ACRONYM') }) + }, + error: function(xhr, status, error) { + self.addMessage({ messages: 'Error creating component: '+error}) + } + })) + }, this) + + $.when.apply($, pp).done(function() { + var cp = [] + self.containers.each(function(c) { + if (c.isNew()) { + cp.push(c.save({}, { + success: function() { + self.addMessage({ message: 'Created container: '+c.get('NAME') }) + }, + error: function(xhr, status, error) { + self.addMessage({ messages: 'Error creating container: '+error}) + } + })) + } + }) + + $.when.apply($, cp).done(function() { + var news = new Samples(self.samples.filter(function(s) { + return s.isNew() + })) + + if (news.length == 0) return + + news.each(function(s) { + if (!s.get('CONTAINERID')) { + var c = self.containers.findWhere({ NAME: s.get('CONTAINER')}) + s.set({ CONTAINERID: c.get('CONTAINERID') }, { silent: true }) + } + + if (s.get('PROTEINID') == 0) { + var p = self.newProteins.findWhere({ ACRONYM: s.get('ACRONYM')}) + s.set({ PROTEINID: p.get('PROTEINID') }, { silent: true }) + } + }) + + news.save({ + success: function() { + app.alert({ message: 'Shipment contents imported, Click here to view it', persist: 'simport'+self.model.escape('SHIPPINGID'), className: 'message notify' }) + self.addMessage({ messages: 'Samples created' }) + }, + error: function(xhr, status, error) { + self.addMessage({ messages: 'Error creating samples: '+error}) + } + }) + }) + + }) + }, + + export: function() { + var rows = [] + rows.push(this.csvProfile.headers) + this.samples.each(function(s) { + var row = [] + _.each(this.csvProfile.mapping, function(k, i) { + if (k in this.csvProfile.export) { + row.push(this.csvProfile.export[k](s.toJSON(), this.csvProfile.headers[i])) + } else { + row.push(s.get(k)) + } + }, this) + + rows.push(row) + }, this) + + var csv = Papa.unparse(rows) + var a = document.createElement('a') + var file = new Blob([csv], {type: 'application/octet-stream'}) + + a.href= URL.createObjectURL(file) + a.download = this.model.get('SHIPPINGNAME') + '.csv' + a.click() + + URL.revokeObjectURL(a.href); + + console.log(csv) + }, + + initialize: function(options) { + this.messages = new Backbone.Collection() + + this.samples = new Samples(null, { state: { pageSize: 9999 }}) + this.samples.queryParams.SHIPPINGID = this.model.get('SHIPPINGID') + this.samples.fetch() + + this.containers = new Containers() + this.containers.queryParams.SHIPPINGID = this.model.get('SHIPPINGID') + this.containers.setSorting('BLTIMESTAMP', 0) + this.containers.fetch() + + this.platetypes = new PlateTypes() + + this.ready = [] + this.containerregistry = new ContainerRegistry(null, { state: { pageSize: 9999 }}) + this.ready.push(this.containerregistry.fetch()) + + this.users = new Users(null, { state: { pageSize: 9999 }}) + this.users.queryParams.all = 1 + this.users.queryParams.pid = app.proposal.get('PROPOSALID') + this.ready.push(this.users.fetch()) + + this.newProteins = new Proteins() + this.proteins = new DistinctProteins() + if (app.valid_samples) { + this.proteins.queryParams.external = 1 + } + this.ready.push(this.proteins.fetch()) + + this.dewars = new Dewars() + this.dewars.queryParams.sid = this.model.get('SHIPPINGID') + this.ready.push(this.dewars.fetch()) + + this.csvProfile = require('csv/'+app.config.csv_profile+'.js') + console.log('initialize', this.csvProfile) + }, + + + dragHover: function(e) { + e.stopPropagation() + e.preventDefault() + if (e.type == 'dragover') this.ui.drop.addClass('active') + else this.ui.drop.removeClass('active') + }, + + uploadFile: function(e) { + this.dragHover(e) + var files = e.originalEvent.dataTransfer.files + var f = files[0] + + if (f.name.endsWith('csv')) { + var reader = new FileReader() + var self = this + reader.onload = function(e) { + self.parseCSVContents(e.target.result) + } + reader.readAsText(f) + } else { + app.alert({ message: 'Cannot import file "'+f.name+'" is not a csv file' }) + } + }, + + createObjects: function(raw) { + var parsed = Papa.parse(raw) + + if (parsed.errors.length) { + var errs = [] + _.each(parsed.errors, function(e) { + errs.push({ message: e.code + ': ' + e.message + ' at row ' + e.row }) + }) + this.messages.reset(errs) + app.alert({ message: 'Error parsing csv file, see messages below' }) + return + } + + var objects = parsed.data + var headers = this.csvProfile.mapping + var transforms = this.csvProfile.transforms + objects.splice(0, 1) + + var newProteins = [] + var populatedObject = [] + _.each(objects, function(item){ + if (!item.length || (item.length == 1 && !item[0].trim())) return + var obj = {} + _.each(item, function(v, i) { + var key = headers[i] + if (v) obj[key] ? obj[key] += ' | '+v : obj[key] = v + }) + + _.each(obj, function(v, k) { + if (k in transforms) { + transforms[k](v, obj) + } + }, this) + + if (!obj.PROTEINID) { + var protein = this.proteins.findWhere({ ACRONYM: obj.ACRONYM }) + if (protein) { + obj.PROTEINID = protein.get('PROTEINID') + } else { + obj.PROTEINID = 0 + var newp = _.findWhere(newProteins, { ACRONYM: obj.ACRONYM }) + if (!newp) { + newProteins.push({ + ACRONYM: obj.ACRONYM, + NAME: obj.ACRONYM, + }) + } + } + } + + populatedObject.push(obj) + }, this) + + this.newProteins.reset(newProteins) + return populatedObject + }, + + parseCSVContents: function(raw) { + var samples = this.createObjects(raw) + console.log('parseCSVContents', samples) + this.samples.reset(samples) + + this.containers.reset(_.map(_.unique(_.pluck(samples, 'CONTAINER')), function(name) { + var sample = this.samples.findWhere({ CONTAINER: name }) + + var ownerid = null + if (sample) { + var oid = this.users.findWhere({ FULLNAME: sample.get('OWNER') }) + if (oid) ownerid = oid.get('PERSONID') + } + + return { NAME: name, DEWARID: this.dewars.at(0).get('DEWARID'), OWNERID: ownerid } + }, this)) + + if (this.newProteins.length) this.ui.pnew.text('Need to create '+this.newProteins.length+' components: '+this.newProteins.pluck('ACRONYM').join(', ')) + }, + + onRender: function() { + $.when.apply($, this.ready).done(this.doOnRender.bind(this)) + this.rmessages.show(new MessagesView({ collection: this.messages })) + }, + + doOnRender: function() { + // this.parseCSVContents(this.csvProfile.exampleCSV) + + this.rcontainers.show(new ContainersView({ + collection: this.containers, + samples: this.samples, + profile: this.csvProfile, + platetypes: this.platetypes, + containerregistry: this.containerregistry, + users: this.users, + proteins: this.proteins, + })) + }, + + }) + +}) diff --git a/client/src/js/modules/shipment/views/queuedcontainers.js b/client/src/js/modules/shipment/views/queuedcontainers.js new file mode 100644 index 000000000..78a162d7f --- /dev/null +++ b/client/src/js/modules/shipment/views/queuedcontainers.js @@ -0,0 +1,217 @@ +define(['backbone', 'marionette', + 'backgrid', + 'views/table', + 'views/filter', + 'utils/table', + 'utils', + 'collections/proposaltypes', + 'collections/bls', + 'collections/containers'], function(Backbone, Marionette, Backgrid, TableView, FilterView, table, utils, + ProposalTypes, + Beamlines, + Containers) { + + + var ClickableRow = table.ClickableRow.extend({ + event: 'container:review', + argument: 'CONTAINERID', + cookie: true, + }) + + var ActionCell = Backgrid.Cell.extend({ + events: { + 'click a.completed': 'markCompleted' + }, + + markCompleted: function(e) { + e.preventDefault() + utils.confirm({ + title: 'Confirm Mark Completed', + content: 'Are you sure you want to mark "'+this.model.get('NAME')+'" completed?', + callback: this.doMarkCompleted.bind(this) + }) + }, + + doMarkCompleted: function() { + var self = this + Backbone.ajax({ + url: app.apiurl+'/shipment/containers/queue/'+this.model.get('CONTAINERQUEUEID'), + method: 'POST', + success: function() { + app.alert({ className: 'message notify', message: 'Container queue successfully marked as completed' }) + self.model.collection.fetch() + }, + error: function() { + app.alert({ message: 'Something went wrong marking this container queue as completed, please try again' }) + }, + + }) + }, + + render: function() { + if (app.staff && this.model.get('CONTAINERQUEUEID')) { + this.$el.html('') + } + + return this + } + }) + + var LocationCell = Backgrid.Cell.extend({ + render: function() { + this.$el.html(this.model.escape('BEAMLINELOCATION')) + if (this.model.get('SAMPLECHANGERLOCATION')) { + this.$el.append(' - ' + this.model.escape('SAMPLECHANGERLOCATION')) + } + return this + } + }) + + var FilterWithDefault = FilterView.extend({ + default: null, + + selected: function() { + var selected = this.collection.findWhere({ isSelected: true }) + if (!selected) { + var selected = this.collection.findWhere({ id: this.getOption('default')}) + selected.set({isSelected: true}) + } + return selected ? selected.get('id') : null + }, + }) + + return Marionette.LayoutView.extend({ + className: 'content', + template: '

Queued Containers (-)

', + regions: { + wrap: '.wrapper', + type: '.type', type2: '.type2', typeas: '.typeas', typebl: '.typebl' + }, + + + hiddenColumns: [3,4,5,7,9,10,11], + + columns: [ + { name: 'NAME', label: 'Name', cell: 'string', editable: false }, + { name: 'PROP', label: 'Proposal', cell: 'string', editable: false }, + { name: 'OWNER', label: 'Owner', cell: 'string', editable: false }, + { name: 'CARDNAME', label: 'Contact', cell: 'string', editable: false }, + { name: 'SHIPMENT', label: 'Shipment', cell: 'string', editable: false }, + { name: 'DEWAR', label: 'Dewar', cell: 'string', editable: false }, + { name: 'SAMPLES', label: '# Samples', cell: 'string', editable: false }, + { name: 'MODES', label: 'Modes', cell: 'string', editable: false }, + { name: 'CONTAINERSTATUS', label: 'Status', cell: 'string', editable: false }, + { name: 'COMMENTS', label: 'Comments', cell: 'string', editable: false }, + { name: 'QUEUEDTIMESTAMP', label: 'Queued', cell: 'string', editable: false }, + { name: 'LASTQUEUECOMPLETED', label: 'Completed', cell: 'string', editable: false }, + { label: 'SC', cell: LocationCell, editable: false }, + { label: '', cell: ActionCell, editable: false }, + ], + + showFilter: true, + filters: [ + { id: 'queued', name: 'Queued'}, + { id: 'completed', name: 'Completed'}, + ], + + ui: { + total: 'span.total', + }, + + refresh: function() { + this.collection.fetch() + }, + + updateTotal: function() { + this.ui.total.text(this.collection.state.totalRecords) + }, + + initialize: function(options) { + this.types = new ProposalTypes() + this.ready = [] + this.ready.push(this.types.fetch()) + + this.beamlines = new Beamlines(null, { ty: app.type }) + this.ready.push(this.beamlines.fetch()) + + this.collection = new Containers() + this.collection.queryParams.all = 1 + this.collection.queryParams.PUCK = 1 + this.collection.queryParams.ty = 'queued' + if (options.params.sid) this.collection.queryParams.SHIPPINGID = options.params.sid + this.collection.state.currentPage = options.params.page + this.listenTo(this.collection, 'sync', this.updateTotal) + + var filters = this.getOption('filters').slice(0) + var columns = this.getOption('columns').slice(0) + + if (app.mobile()) { + _.each(this.getOption('hiddenColumns'), function(v) { + columns[v].renderable = false + }) + } + + this.table = new TableView({ + collection: this.collection, + columns: columns, + tableClass: 'containers', filter: 's', search: options.params.s, loading: true, + backgrid: { row: ClickableRow, emptyText: 'No containers found' } }) + + this.ty = new FilterWithDefault({ + default: 'queued', + collection: this.collection, + value: options.params && options.params.ty, + name: 'ty', + filters: filters + }) + + this.assigned = new FilterView({ + collection: this.collection, + name: 'assigned', + filters: { id: 1, name: 'Assigned'}, + }) + }, + + onRender: function() { + this.wrap.show(this.table) + this.type.show(this.ty) + this.typeas.show(this.assigned) + + $.when.apply($, this.ready).then(this.doOnRender.bind(this)) + }, + + doOnRender: function() { + this.showProposalFilter() + this.showBeamlineFilter() + this.refresh() + }, + + showProposalFilter: function() { + this.ty2 = new FilterView({ + collection: this.collection, + name: 'PROPOSALCODE', + urlFragment: 'pt', + value: this.getOption('params') && this.getOption('params').pt, + filters: this.types.map(function(b) { return { id: b.get('PROPOSALCODE'), name: b.get('PROPOSALCODE') } }), + }) + this.type2.show(this.ty2) + }, + + updateFilter2: function(selected) { + this.collection.queryParams.proposalcode = selected + this.refresh() + }, + + showBeamlineFilter: function() { + this.tybl = new FilterView({ + collection: this.collection, + name: 'bl', + urlFragment: 'bl', + value: this.getOption('params') && this.getOption('params').bl, + filters: this.beamlines.map(function(b) { return { id: b.get('BEAMLINE'), name: b.get('BEAMLINE') } }), + }) + this.typebl.show(this.tybl) + }, + }) + +}) diff --git a/client/src/js/modules/shipment/views/sampletable.js b/client/src/js/modules/shipment/views/sampletable.js index fb9c372de..211f29b36 100644 --- a/client/src/js/modules/shipment/views/sampletable.js +++ b/client/src/js/modules/shipment/views/sampletable.js @@ -16,6 +16,7 @@ define(['marionette', 'utils/centringmethods', 'utils/experimentkinds', 'utils/radiationsensitivity', + 'utils/collectionmode', 'utils', 'utils/safetylevel', @@ -23,7 +24,7 @@ define(['marionette', 'jquery', ], function(Marionette, Protein, Proteins, ValidatedRow, DistinctProteins, ComponentsView, sampletable, sampletablerow, sampletablerowedit, - forms, SG, Anom, CM, EXP, RS, utils, safetyLevel, $) { + forms, SG, Anom, CM, EXP, RS, COLM, utils, safetyLevel, $) { // A Sample Row @@ -73,13 +74,15 @@ define(['marionette', cancelEditSample: function(e) { this.editing = false e.preventDefault() + if (this.model.get('PROTEINID') > -1 && this.model.isNew()) this.model.set({ PROTEINID: -1, CRYSTALID: -1 }) this.template = this.getOption('rowTemplate') this.render() }, setData: function() { var data = {} - _.each(['CODE', 'PROTEINID', 'CRYSTALID', 'NAME', 'COMMENTS', 'SPACEGROUP', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'LOOPTYPE', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'ENERGY', 'RADIATIONSENSITIVITY', 'USERPATH'], function(f) { + _.each(['CODE', 'PROTEINID', 'CRYSTALID', 'NAME', 'COMMENTS', 'SPACEGROUP', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'LOOPTYPE', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'ENERGY', 'RADIATIONSENSITIVITY', 'USERPATH', + 'AIMEDRESOLUTION', 'COLLECTIONMODE', 'PRIORITY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'PREFERREDBEAMSIZEX'], function(f) { var el = this.$el.find('[name='+f+']') if (el.length) data[f] = el.attr('type') == 'checkbox'? (el.is(':checked')?1:null) : el.val() }, this) @@ -148,6 +151,7 @@ define(['marionette', CELL_A: '', CELL_B: '', CELL_C: '', CELL_ALPHA: '', CELL_BETA: '', CELL_GAMMA: '', REQUIREDRESOLUTION: '', ANOM_NO: '', ANOMALOUSSCATTERER: '', CRYSTALID: -1, PACKINGFRACTION: '', LOOPTYPE: '', DIMENSION1: '', DIMENSION2: '', DIMENSION3: '', SHAPE: '', CENTRINGMETHOD: '', EXPERIMENTKIND: '', ENERGY: '', RADIATIONSENSITIVITY: '', USERPATH: '', + AIMEDRESOLUTION: '', COLLECTIONMODE: '', PRIORITY: '', EXPOSURETIME: '', AXISSTART: '', AXISRANGE: '', NUMBEROFIMAGES: '', TRANSMISSION: '', PREFERREDBEAMSIZEX: '' }) this.model.get('components').reset() this.render() @@ -197,13 +201,15 @@ define(['marionette', //if (this.model.get('CODE')) this.$el.find('input[name=CODE]').val(this.model.get('CODE')) //if (this.model.get('COMMENTS')) this.$el.find('input[name=COMMENTS]').val(this.model.get('COMMENTS')) - _.each(['NAME', 'CODE', 'COMMENTS', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'REQUIREDRESOLUTION', 'ANOM_NO', 'VOLUME', 'PACKINGFRACTION', 'USERPATH'], function(f, i) { + _.each(['NAME', 'CODE', 'COMMENTS', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'REQUIREDRESOLUTION', 'ANOM_NO', 'VOLUME', 'PACKINGFRACTION', 'USERPATH', + 'AIMEDRESOLUTION', 'COLLECTIONMODE', 'PRIORITY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'PREFERREDBEAMSIZEX'], function(f, i) { if (this.model.get(f)) this.$el.find('input[name='+f+']').val(this.model.get(f)) }, this) this.ui.symbol.text(this.model.get('SYMBOL') ? this.model.get('SYMBOL') : '') if (this.getOption('extra').show) this.$el.find('.extra').addClass('show') + if (this.getOption('dp').show) this.$el.find('.dp').addClass('show') if (this.getOption('type') == 'non-xtal') { this.$el.find('.non-xtal').addClass('show') @@ -224,6 +230,7 @@ define(['marionette', this.$el.find('[name=EXPERIMENTKIND]').html(EXP.opts()).val(this.model.get('EXPERIMENTKIND')) this.$el.find('[name=ENERGY]').val(this.model.get('ENERGY')) this.$el.find('[name=RADIATIONSENSITIVITY]').html(RS.opts()).val(this.model.get('RADIATIONSENSITIVITY')) + this.$el.find('[name=COLLECTIONMODE]').html(COLM.opts()).val(this.model.get('COLLECTIONMODE')) this.compview = new ComponentsView({ collection: this.model.get('components'), editable: this.editing || this.model.get('new') }) this.ui.comps.append(this.compview.render().$el) @@ -364,9 +371,12 @@ define(['marionette', if (options.childEditTemplate) this.options.childViewOptions.editTemplate = options.childEditTemplate this.extra = { show: false } + this.dp = { show: false } this.auto = { show: options.auto == true ? true : false } + this.options.childViewOptions.extra = this.extra this.options.childViewOptions.auto = this.auto + this.options.childViewOptions.dp = this.dp this.options.childViewOptions.type = this.getOption('type') }, @@ -388,6 +398,10 @@ define(['marionette', return this.extra.show }, + dpState: function() { + return this.dp.show + }, + toggleExtra: function() { this.extra.show = !this.extra.show @@ -395,6 +409,12 @@ define(['marionette', else this.$el.find('.extra').removeClass('show') }, + toggleDP: function() { + this.dp.show = !this.dp.show + + if (this.dp.show) this.$el.find('.dp').addClass('show') + else this.$el.find('.dp').removeClass('show') + }, toggleAuto: function(val) { this.auto.show = val diff --git a/client/src/js/modules/shipment/views/shipment.js b/client/src/js/modules/shipment/views/shipment.js index d9c9051cf..3e6440ad4 100644 --- a/client/src/js/modules/shipment/views/shipment.js +++ b/client/src/js/modules/shipment/views/shipment.js @@ -43,14 +43,19 @@ define(['marionette', APIURL: app.apiurl, PROP: app.prop, DHL_ENABLE: app.options.get('dhl_enable'), + IS_STAFF: app.staff, + QUEUE_SHIPMENT: app.config.queue_shipment, + AUTO_LABEL: app.config.auto_collect_label || 'Automated' } }, events: { 'click #add_dewar': 'addDewar', 'click a.send': 'sendShipment', + 'click a.return': 'returnShipment', 'click a.pdf': utils.signHandler, 'click a.cancel_pickup': 'cancelPickup', + 'click a.queue': 'queueShipment', }, ui: { @@ -101,7 +106,7 @@ define(['marionette', Backbone.ajax({ url: app.apiurl+'/shipment/send/'+this.model.get('SHIPPINGID'), success: function() { - self.model.set({ SHIPPINGSTATUS: 'send to DLS' }) + self.model.set({ SHIPPINGSTATUS: 'sent to facility' }) app.alert({ className: 'message notify', message: 'Shipment successfully marked as sent' }) self.render() }, @@ -111,7 +116,83 @@ define(['marionette', }) }, - + + returnShipment: function(e) { + e.preventDefault() + var self = this + Backbone.ajax({ + url: app.apiurl+'/shipment/return/'+this.model.get('SHIPPINGID'), + success: function() { + self.model.set({ SHIPPINGSTATUS: 'returned' }) + self.render() + setTimeout(function() { + app.alert({ className: 'message notify', message: 'Shipment successfully marked as returned to user' }) + }, 500) + + }, + error: function(xhr) { + var json = {}; + if (xhr.responseText) { + try { + json = JSON.parse(xhr.responseText) + } catch(err) { + + } + } + + if (json.message) { + app.alert({ message: json.message }) + } else { + app.alert({ message: 'Something went wrong marking this shipment returned, please try again' }) + } + }, + + }) + }, + + queueShipment: function(e) { + e.preventDefault() + + var containers = new Containers() + // make sure to return all containers in the shipment + containers.state.pageSize = 100 + containers.queryParams.SHIPPINGID = this.model.get('SHIPPINGID') + containers.fetch().done(function () { + var promises = [] + var success = 0 + var failure = 0 + + containers.each(function(c) { + promises.push(Backbone.ajax({ + url: app.apiurl+'/shipment/containers/queue', + data: { + CONTAINERID: c.get('CONTAINERID') + }, + success: function() { + success++ + }, + error: function(xhr) { + var json = {}; + if (xhr.responseText) { + try { + json = JSON.parse(xhr.responseText) + } catch(err) { + + } + } + app.alert({ message: c.get('CONTAINERID') + ': ' + json.message }) + failure++ + } + })) + }) + + $.when.apply($, promises).then(function() { + app.alert({ message: success+ ' Container(s) Successfully Queued, ' + failure + ' Failed' }) + }).fail(function() { + app.alert({ message: success+ ' Container(s) Successfully Queued, ' + failure + ' Failed' }) + }) + }) + }, addDewar: function(e) { e.preventDefault() diff --git a/client/src/js/modules/types/mx/menu.js b/client/src/js/modules/types/mx/menu.js index 04a3b3d4f..3c5cdc912 100644 --- a/client/src/js/modules/types/mx/menu.js +++ b/client/src/js/modules/types/mx/menu.js @@ -23,6 +23,7 @@ define([], function() { }, admin: { + 'containers/queued': { title: 'Queue', icon: 'database', permission: 'auto_dash' }, 'runs/overview': { title: 'Run Overview', icon: 'bar-chart', permission: 'all_breakdown' }, 'stats/overview/beamlines': { title: 'Reporting', icon: 'line-chart', permission: 'all_prop_stats' }, 'admin/imaging': { title: 'Imaging', icon: 'image', permission: 'imaging_dash' }, diff --git a/client/src/js/templates/assign/scanassign.html b/client/src/js/templates/assign/scanassign.html new file mode 100644 index 000000000..75de719b5 --- /dev/null +++ b/client/src/js/templates/assign/scanassign.html @@ -0,0 +1,7 @@ +

Container Allocation for <%-bl%>

+ +

This page is designed to allocate containers via barcode scanning. Click on a sample change position and scan a barcode to assign it to that position. You will be presented with a list of matching containers.

+ +
+
+
diff --git a/client/src/js/templates/samples/sample.html b/client/src/js/templates/samples/sample.html index 7d18df4c3..4e5d62030 100644 --- a/client/src/js/templates/samples/sample.html +++ b/client/src/js/templates/samples/sample.html @@ -134,7 +134,7 @@

Sample Details

<% if (CONTAINERQUEUEID) { %>
  • - Auto Collect Queued + <%-AUTO_LABEL%> Collection Queued <%-QUEUEDTIMESTAMP%>
  • diff --git a/client/src/js/templates/shipment/container.html b/client/src/js/templates/shipment/container.html index 12bc4eadd..7f3f160d1 100644 --- a/client/src/js/templates/shipment/container.html +++ b/client/src/js/templates/shipment/container.html @@ -7,6 +7,10 @@

    Container: <%-NAME%>

    This container is currently assigned and in use on a beamline sample changer. Unassign it to make it editable

    <% } %> +
    + Review +
    +
    @@ -48,7 +52,7 @@

    Container: <%-NAME%>

  • - Automated Collection + <%-AUTO_LABEL%> Collection
  • <% if (EXPERIMENTTYPE) { %> @@ -83,5 +87,8 @@

    Container: <%-NAME%>

    +<% if (ENABLE_EXP_PLAN) { %> + Plan Fields +<% } %> Extra Fields
    \ No newline at end of file diff --git a/client/src/js/templates/shipment/containeradd.html b/client/src/js/templates/shipment/containeradd.html index db492aa83..e60d540a9 100644 --- a/client/src/js/templates/shipment/containeradd.html +++ b/client/src/js/templates/shipment/containeradd.html @@ -45,7 +45,7 @@

    Add New Container

  • - +
  • @@ -126,6 +126,10 @@

    Add New Container

    Clone from First Sample Clear Puck + + <% if (ENABLE_EXP_PLAN) { %> + Plan Fields + <% } %> Extra Fields
    diff --git a/client/src/js/templates/shipment/containerreview.html b/client/src/js/templates/shipment/containerreview.html new file mode 100644 index 000000000..9dc21bd16 --- /dev/null +++ b/client/src/js/templates/shipment/containerreview.html @@ -0,0 +1,42 @@ +

    Review: <%-NAME%>

    + + +

    This page shows the data collection and sample status of the selected container.

    + +
    + View Container +
    + +
    +
    + + +
    + +
    diff --git a/client/src/js/templates/shipment/fromcsv.html b/client/src/js/templates/shipment/fromcsv.html new file mode 100644 index 000000000..84ae03a6c --- /dev/null +++ b/client/src/js/templates/shipment/fromcsv.html @@ -0,0 +1,19 @@ +

    <%-SHIPPINGNAME%>: Import from CSV

    + Export to CSV + +
    + Drop CSV File Here +
    + +

    Messages

    +
    +
    + +
    + +
    + +
    diff --git a/client/src/js/templates/shipment/fromcsvcontainer.html b/client/src/js/templates/shipment/fromcsvcontainer.html new file mode 100644 index 000000000..eea78f63b --- /dev/null +++ b/client/src/js/templates/shipment/fromcsvcontainer.html @@ -0,0 +1,5 @@ +
    + Type: + Registered Container: + Owner: +
    diff --git a/client/src/js/templates/shipment/fromcsvtable.html b/client/src/js/templates/shipment/fromcsvtable.html new file mode 100644 index 000000000..696e046e5 --- /dev/null +++ b/client/src/js/templates/shipment/fromcsvtable.html @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/js/templates/shipment/sampletable.html b/client/src/js/templates/shipment/sampletable.html index e61e0ada6..d78885c6a 100644 --- a/client/src/js/templates/shipment/sampletable.html +++ b/client/src/js/templates/shipment/sampletable.html @@ -21,6 +21,16 @@ Components Unit Cell + + Aimed Res + Mode + Priority + Exposure + Axis Start + Axis Range + No. Images + Transmission + Beamsize Status   diff --git a/client/src/js/templates/shipment/sampletablenew.html b/client/src/js/templates/shipment/sampletablenew.html index f253cbbd9..df23ae1b9 100644 --- a/client/src/js/templates/shipment/sampletablenew.html +++ b/client/src/js/templates/shipment/sampletablenew.html @@ -23,6 +23,16 @@ Unit Cell + Aimed Res + Mode + Priority + Exposure + Axis Start + Axis Range + No. Images + Transmission + Beamsize +   diff --git a/client/src/js/templates/shipment/sampletablerow.html b/client/src/js/templates/shipment/sampletablerow.html index bf4d40e61..e63012469 100644 --- a/client/src/js/templates/shipment/sampletablerow.html +++ b/client/src/js/templates/shipment/sampletablerow.html @@ -7,6 +7,7 @@       +   <% } else { %> @@ -59,6 +60,16 @@ + <%-AIMEDRESOLUTION%> + <%-COLLECTIONMODE%> + <%-PRIORITY%> + <%-EXPOSURETIME%> + <%-AXISSTART%> + <%-AXISRANGE%> + <%-NUMBEROFIMAGES%> + <%-TRANSMISSION%> + <%-PREFERREDBEAMSIZEX%> + <% if (BLSAMPLEID && STATUS) { %>