From 15bf0e11966ce11be88f353d3f044f4dde186fb3 Mon Sep 17 00:00:00 2001 From: Rida Abou-Haidar Date: Thu, 27 Apr 2023 12:34:53 -0400 Subject: [PATCH 01/19] implementing user site match permissions on entities properly removing CenterID from Pools finalizing changes updates added site filtering to shipments updating entities to include user-readable values adding specimen barcodes to pools attempts at resolving front-end container display changed filter type for containers implementing user site match permissions on entities properly removing CenterID from Pools finalizing changes updating entities to include user-readable values adding specimen barcodes to pools attempts at resolving front-end container display --- css/._biobank.css | Bin 0 -> 4096 bytes css/biobank.css | 2 +- jsx/biobankIndex.js | 24 ++--- jsx/container.js | 12 ++- jsx/containerDisplay.js | 8 +- jsx/containerParentForm.js | 7 +- jsx/globals.js | 30 +++--- jsx/poolSpecimenForm.js | 1 - jsx/poolTab.js | 36 ++++--- jsx/shipmentTab.js | 7 +- jsx/specimenTab.js | 44 ++++---- php/container.class.inc | 95 +++++++++++++---- php/containercontroller.class.inc | 89 +++------------- php/containerdao.class.inc | 125 +++++++++++----------- php/containerendpoint.class.inc | 8 +- php/log.class.inc | 28 ++--- php/module.class.inc | 2 +- php/optionsendpoint.class.inc | 26 ++--- php/pool.class.inc | 117 +++++++++++++++++--- php/poolcontroller.class.inc | 28 ++--- php/pooldao.class.inc | 66 ++++++++---- php/poolendpoint.class.inc | 8 +- php/shipment.class.inc | 75 ++++++++----- php/shipmentdao.class.inc | 5 +- php/shipmenthandler.class.inc | 24 +---- php/shipments.class.inc | 29 +++-- php/specimen.class.inc | 172 ++++++++++++++++++++++-------- php/specimencontroller.class.inc | 2 + php/specimendao.class.inc | 45 ++++++-- php/specimenendpoint.class.inc | 2 +- 30 files changed, 679 insertions(+), 438 deletions(-) create mode 100644 css/._biobank.css diff --git a/css/._biobank.css b/css/._biobank.css new file mode 100644 index 0000000000000000000000000000000000000000..b154692cfe3ac150dbaad234b1f78af8c76a65de GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vVqhrZ zfaqYD0aVV7riBs6hl-0P=jZAr78K;9>J=2_m!;+<<|U^x02R!aKeXnEnFYx5Q7{?; zqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Ulnv0Myw8VK9&j$;d2LC`v8PFD*(= zRY=P(%2vqCD@n~O$;{77%*m-#$Vp8rQAo;3%*zJ$g`v8JG==JaxL0Ht { - const container = containers[id]; - if (container.coordinate) { - result[container.coordinate] = id; - } - return result; - }, {}); - + const coordinates = this.state.data.containers[parentContainerId].coordinates; + const increment = (coord) => { - coord++; - if (childCoordinates.hasOwnProperty(coord)) { - coord = increment(coord); - } - - return coord; + return coordinates.includes(coord) ? increment(coord + 1) : coord; }; - + return increment(coordinate); } @@ -436,6 +423,9 @@ class BiobankIndex extends React.Component { .reduce((result, item) => { item.container.statusId = dispensedId; item.specimen.quantity = '0'; + // XXX: By updating the container and specimen after, it's causing issues + // if they don't meet validation. The error is being thrown only after the + // pool has already been saved to the database! Not sure how to resolve this. return [...result, () => this.updateContainer(item.container, false), () => this.updateSpecimen(item.specimen, false), diff --git a/jsx/container.js b/jsx/container.js index fb5a4654..99d39186 100644 --- a/jsx/container.js +++ b/jsx/container.js @@ -79,7 +79,11 @@ class BiobankContainer extends Component { const coordinates = data.containers[container.id].childContainerIds .reduce((result, id) => { const container = data.containers[id]; - if (container.coordinate) { + if (container === undefined) { + // if the container is undefined, user does not have permission + // to view it. + console.log('undefined'); + } else if (container.coordinate) { result[container.coordinate] = id; } return result; @@ -125,7 +129,11 @@ class BiobankContainer extends Component { } const child = data.containers[childId]; - if (child.coordinate) { + + if (child === undefined) { + // if the child container is undefined, user does not have permission + // to view it. + } else if (child.coordinate) { listAssigned.push(
{ const container = data.containers[id]; - if (container.coordinate) { - result[container.coordinate] = id; + if (container == undefined) { + // if the container is undefined, the user doesn't have permission to + // to view it + } else if (container.coordinate) { + result[container.coordinate] = id; } return result; }, {}); diff --git a/jsx/globals.js b/jsx/globals.js index b94616d3..97e7bbc3 100644 --- a/jsx/globals.js +++ b/jsx/globals.js @@ -266,21 +266,23 @@ function Globals(props) { }; const parentSpecimenField = () => { - if ((specimen||{}).parentSpecimenIds) { - const parentSpecimenBarcodes = Object.values(specimen.parentSpecimenIds) - .map((id) => { - const barcode = data.containers[data.specimens[id].containerId].barcode; - return {barcode}; - }) - .reduce((prev, curr) => [prev, ', ', curr], []); - - return ( - - ); + if (!specimen) { + return null; // Return null if specimen is undefined or null to handle edge case } + + const { parentSpecimenIds, parentSpecimenBarcodes } = specimen; + const value = parentSpecimenIds?.length === 0 + ? 'None' + : Object.values(parentSpecimenBarcodes) + .map(barcode => {barcode}) + .join(', '); + + return ( + + ); }; // TODO: Find a way to make this conform to the GLOBAL ITEM structure. diff --git a/jsx/poolSpecimenForm.js b/jsx/poolSpecimenForm.js index c3a82109..ff7ef701 100644 --- a/jsx/poolSpecimenForm.js +++ b/jsx/poolSpecimenForm.js @@ -76,7 +76,6 @@ class PoolSpecimenForm extends React.Component { // Set current pool values const specimenIds = pool.specimenIds || []; specimenIds.push(specimen.id); - pool.centerId = container.centerId; pool.specimenIds = specimenIds; this.setState( diff --git a/jsx/poolTab.js b/jsx/poolTab.js index dba1abb8..24da983f 100644 --- a/jsx/poolTab.js +++ b/jsx/poolTab.js @@ -64,10 +64,6 @@ class PoolTab extends Component { mapPoolColumns(column, value) { const {data, options} = this.props; switch (column) { - case 'Pooled Specimens': - return value.map((id) => { - return (data.containers[data.specimens[id].containerId]||{}).barcode; - }); case 'Type': return options.specimen.types[value].label; case 'Site': @@ -91,25 +87,33 @@ class PoolTab extends Component { value = this.mapPoolColumns(column, value); const candId = Object.values(options.candidates) .find((cand) => cand.pscid == row['PSCID']).id; + + // If candId is defined, then the user has access to the candidate and a + // hyperlink can be established. + const candidatePermission = candId !== undefined; switch (column) { case 'Pooled Specimens': const barcodes = value .map((barcode, i) => { - if (loris.userHasPermission('biobank_specimen_view')) { - return {barcode}; - } + return {barcode}; }) .reduce((prev, curr) => [prev, ', ', curr]); return {barcodes}; case 'PSCID': - return {value}; + if (candidatePermission) { + return {value}; + } + return {value}; case 'Visit Label': - const ses = Object.values(options.candidateSessions[candId]).find( - (sess) => sess.label == value - ).id; - const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ - '&sessionID='+ses; - return {value}; + if (candidatePermission) { + const ses = Object.values(options.candidateSessions[candId]).find( + (sess) => sess.label == value + ).id; + const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ + '&sessionID='+ses; + return {value}; + } + return {value}; case 'Aliquot': const onClick = () => this.openAliquotForm(row['ID']); return ; @@ -175,8 +179,8 @@ class PoolTab extends Component { Math.round(pool.quantity*100)/100 + ' ' + options.specimen.units[pool.unitId].label, - pool.specimenIds, - options.candidates[pool.candidateId].pscid, + pool.specimenBarcodes, + pool.candidatePSCID, options.sessions[pool.sessionId].label, pool.typeId, pool.centerId, diff --git a/jsx/shipmentTab.js b/jsx/shipmentTab.js index 0697bed7..79685463 100644 --- a/jsx/shipmentTab.js +++ b/jsx/shipmentTab.js @@ -74,7 +74,6 @@ function ShipmentTab({ > @@ -171,7 +170,6 @@ function ShipmentTab({ */ function ShipmentInformation({ shipment, - containers = {}, centers, }) { const logs = shipment.logs.map((log, i) => { @@ -211,8 +209,7 @@ function ShipmentInformation({ ); }); - const containerBarcodes = shipment.containerIds.map((id, i) => { - const barcode = (containers[id] || {}).barcode; + const containerBarcodes = shipment.containerBarcodes.map((barcode, i) => { return ( data.containers[data.specimens[id].containerId].barcode); + case 'Diagnosis': + if (value) { + return value.map((id) => options.diagnoses[id].label); } break; - case 'Diagnosis': - return value.map((id) => options.diagnoses[id].label); case 'Status': return options.container.stati[value].label; case 'Current Site': @@ -96,23 +93,31 @@ class SpecimenTab extends Component { value = this.mapSpecimenColumns(column, value); const candId = Object.values(options.candidates) .find((cand) => cand.pscid == row['PSCID']).id; + const candidatePermission = candId !== undefined; switch (column) { case 'Barcode': return {value}; case 'Parent Specimens': + // TODO: if the user doesn't have access then these shouldn't be hyperlinked const barcodes = value && value.map((id, key) => { return {value}; }).reduce((prev, curr) => [prev, ', ', curr]); return {barcodes}; case 'PSCID': - return {value}; + if (candidatePermission) { + return {value}; + } + return {value}; case 'Visit Label': - const ses = Object.values(options.candidateSessions[candId]).find( - (sess) => sess.label == value - ).id; - const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ - '&sessionID='+ses; - return {value}; + if (candidatePermission) { + const ses = Object.values(options.candidateSessions[candId]).find( + (sess) => sess.label == value + ).id; + const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ + '&sessionID='+ses; + return {value}; + } + return {value}; case 'Status': const style = {}; switch (value) { @@ -162,8 +167,6 @@ class SpecimenTab extends Component { const diagnoses = mapFormOptions(options.diagnoses, 'label'); const specimenData = Object.values(data.specimens).map((specimen) => { const container = data.containers[specimen.containerId]; - const pID = container.parentContainerId; - const parentContainer = data.containers[pID] || {}; let specimenAttributeData = []; Object.keys(options.specimen.processAttributes) .forEach((processId) => { @@ -184,17 +187,18 @@ class SpecimenTab extends Component { } }); }); + const candidate = options.candidates[specimen.candidateId]; return [ container.barcode, specimen.typeId, container.typeId, specimen.quantity+' '+options.specimen.units[specimen.unitId].label, specimen.fTCycle || null, - specimen.parentSpecimenIds, - options.candidates[specimen.candidateId].pscid, - options.candidates[specimen.candidateId].sex, + specimen.parentSpecimenBarcodes, + specimen.candidatePSCID, + candidate?.sex || null, specimen.candidateAge, - options.candidates[specimen.candidateId].diagnosisIds, + candidate?.diagnosis || null, options.sessions[specimen.sessionId].label, specimen.poolId ? (data.pools[specimen.poolId]||{}).label : null, container.statusId, @@ -204,7 +208,7 @@ class SpecimenTab extends Component { specimen.collection.date, specimen.collection.time, (specimen.preparation||{}).time, - parentContainer.barcode, + container.parentContainerBarcode, container.coordinate, ...specimenAttributeData, ]; diff --git a/php/container.class.inc b/php/container.class.inc index 34e18267..f7e7f0d2 100644 --- a/php/container.class.inc +++ b/php/container.class.inc @@ -30,7 +30,10 @@ namespace LORIS\biobank; * @link https://www.github.com/aces/Loris/ */ -class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyEntities\SiteHaver +class Container implements + \JsonSerializable, + \LORIS\Data\DataInstance, + \LORIS\StudyEntities\SiteHaver { /** * Persistent Instance variables. @@ -48,7 +51,9 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S * @var array $shipmentBarocdes * @var \CenterID $centerId * @var int $parentContainerId + * @var string $parentContainerBarocde * @var array $childContainerIds + * @var array $coordinates * @var int $coordinate * @var string $lotNumber * @var \DateTime $expirationDate @@ -67,7 +72,9 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S private $shipmentBarcodes; private \CenterID $centerId; private $parentContainerId; + private string $parentContainerBarocde; private $childContainerIds; + private array $coordinates; private $coordinate; private $lotNumber; private $expirationDate; @@ -305,7 +312,7 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S * * @return \CenterID */ - public function getCenterID() : \CenterID + public function getCenterId() : \CenterID { return $this->centerId; } @@ -332,6 +339,28 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S return $this->parentContainerId; } + /** + * Sets the parent container's barcode + * + * @param ?int $parentContainerBarcode the parent container's barcode + * + * @return void + */ + public function setParentContainerBarcode(?string $parentContainerBarcode) : void + { + $this->parentContainerBarcode = $parentContainerBarcode; + } + + /** + * Gets the parent container's barcode + * + * @return ?string + */ + public function getParentContainerBarcode() : ?string + { + return $this->parentContainerBarcode; + } + /** * Sets the IDs of the children containers * @@ -354,6 +383,28 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S return $this->childContainerIds; } + /** + * Sets the coordinates of the children containers + * + * @param array $coordinates + * + * @return void + */ + public function setCoordinates(array $coordinates) : void + { + $this->coordinates = $coordinates; + } + + /** + * Gets the coordinates of the children containers + * + * @return ?array + */ + public function getCoordinates() : ?array + { + return $this->coordinates; + } + /** * Sets the container's current coordinate in storage * @@ -486,9 +537,15 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S if (isset($data['parentContainerId'])) { $this->setParentContainerId((int) $data['parentContainerId']); } + if (isset($data['parentContainerBarcode'])) { + $this->setParentContainerBarcode((string) $data['parentContainerBarcode']); + } if (isset($data['childContainerIds'])) { $this->setChildContainerIds($data['childContainerIds']); } + if (isset($data['coordinates'])) { + $this->setCoordinates($data['coordinates']); + } if (isset($data['coordinate'])) { $this->setCoordinate((int) $data['coordinate']); } @@ -529,22 +586,24 @@ class Container implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\S } return [ - 'id' => $this->id, - 'barcode' => $this->barcode, - 'specimenId' => $this->specimenId, - 'typeId' => $this->typeId, - 'dimensionId' => $this->dimensionId, - 'temperature' => $this->temperature, - 'statusId' => $this->statusId, - 'projectIds' => $this->projectIds, - 'shipmentBarcodes' => $this->shipmentBarcodes, - 'centerId' => $this->centerId->__toString(), - 'parentContainerId' => $this->parentContainerId, - 'childContainerIds' => $this->childContainerIds, - 'coordinate' => $this->coordinate, - 'lotNumber' => $this->lotNumber, - 'expirationDate' => $expirationDate, - 'comments' => $this->comments, + 'id' => $this->id, + 'barcode' => $this->barcode, + 'specimenId' => $this->specimenId, + 'typeId' => $this->typeId, + 'dimensionId' => $this->dimensionId, + 'temperature' => $this->temperature, + 'statusId' => $this->statusId, + 'projectIds' => $this->projectIds, + 'shipmentBarcodes' => $this->shipmentBarcodes, + 'centerId' => $this->centerId, + 'parentContainerId' => $this->parentContainerId, + 'parentContainerBarcode' => $this->getParentContainerBarcode, + 'childContainerIds' => $this->childContainerIds, + 'coordinates' => $this->coordinates, + 'coordinate' => $this->coordinate, + 'lotNumber' => $this->lotNumber, + 'expirationDate' => $expirationDate, + 'comments' => $this->comments, ]; } diff --git a/php/containercontroller.class.inc b/php/containercontroller.class.inc index b9313d2a..037c6e6c 100644 --- a/php/containercontroller.class.inc +++ b/php/containercontroller.class.inc @@ -69,12 +69,14 @@ class ContainerController /** * Get all Container Objects permitted by the Container Data Provisioner (DAO). * - * @return array $specimens All permissable Container Objects + * @return array $containers All permissable Container Objects */ public function getInstances() : array { $this->_validatePermission('view'); $containers = []; + $this->dao = $this->dao + ->filter(new \LORIS\Data\Filters\UserProjectOrSiteMatch()); $containerIt = $this->dao->execute($this->user); foreach ($containerIt as $id => $container) { $containers[$id] = $container; @@ -151,6 +153,8 @@ class ContainerController } /** + * XXX: This function can likely be delete completely and the dao can be + * instatiated at the beginning. This filters can be add at the API endpoint. * Treats the Container DAO as a Provisioner that can be iterated through * to provide the permissable Container Objects for the current User. * @@ -159,15 +163,15 @@ class ContainerController private function _getDataProvisioner() : ContainerDAO { $dao = new ContainerDAO($this->db); - if ($this->user->hasPermission('access_all_profiles') === false) { - $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - } - if ($this->user->hasPermission('biobank_container_view') === false) { - $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 0)); - } - if ($this->user->hasPermission('biobank_specimen_view') === false) { - $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 1)); - } + // if ($this->user->hasPermission('access_all_profiles') === false) { + // $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); + // } + // if ($this->user->hasPermission('biobank_container_view') === false) { + // $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 0)); + // } + // if ($this->user->hasPermission('biobank_specimen_view') === false) { + // $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 1)); + // } return $dao; } @@ -379,71 +383,6 @@ class ContainerController } } - /** - * Validates Container Object Project ID. - * - * @param Container $container Container to be checked. - * - * @throws BadRequest if the provided Container does not meet validation - * requirements - * - * @return void - */ - private function _validateProjectIds(Container $container) : void - { - $projectIds = $container->getProjectIds(); - - if (empty($projectIds)) { - throw new BadRequest("A Project must be assigned"); - } - - foreach ($projectIds as $id) { - if (empty($id)) { - throw new BadRequest("All project values must be valid."); - } - } - - // Check that current container's projects are a subset of the parent - // Container's projects. - $parentContainerId = $container->getParentContainerId(); - if (false) { - $parentContainer = $this->dao->getInstanceFromId($parentContainerId); - $parentProjectIds = $parentContainer->getProjectIds(); - if (array_intersect($projectIds, $parentProjectIds) != $projectIds) { - $barcode = $container->getBarcode(); - $parentBarcode = $parentContainer->getBarcode(); - throw new BadRequest( - "The Projects to which $barcode - belongs must be a subset of the Projects to - which $parentBarcode - belongs." - ); - } - } - - // Check the current container's projects are a superset of the child - // Containers' projects. - $childContainerIds = $container->getChildContainerIds(); - if ($childContainerIds) { - forEach ($childContainerIds as $id) { - $childContainer = $this->dao->getInstanceFromId((int) $id); - $childProjectIds = $childContainer->getProjectIds(); - if (array_intersect( - $childProjectIds, - $projectIds - ) != $childProjectIds - ) { - $barcode = $container->getBarcode(); - throw new BadRequest( - "The projects to which $barcode - belongs must be a superset - of it's child Containers' Projects" - ); - } - } - } - } - /** * Validates Container Object Shipment Barcodes. * diff --git a/php/containerdao.class.inc b/php/containerdao.class.inc index 058846d9..a8937054 100644 --- a/php/containerdao.class.inc +++ b/php/containerdao.class.inc @@ -79,24 +79,32 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance )[$id] ?? null; } - /** - * Returns an array of all the Child Container IDs associated with - * the given Container ID from the biobank_container_parent table. - * - * @param ?int $id of Container - * - * @return array $childContainerIds List of Container IDs that are - * children of the Container ID - */ - private function _getChildContainerIds(?int $id) : array - { - $query = 'SELECT ContainerID - FROM biobank_container_parent - WHERE ParentContainerID=:i'; - $childContainerIds = $this->db->pselectCol($query, ['i' => $id]); - - return $childContainerIds; - } + // /** + // * Returns an array of all the Child Container Barcodes associated with + // * the given Container ID from the biobank_container_parent table. + // * + // * @param ?int $id + // * + // * @return array $childContainerBarocdes + // */ + // private function _getChildContainerBarcodes(?int $id) : array + // { + // $query = 'SELECT bcp.Coordinate, + // bc2.Barcode + // FROM biobank_container bc + // LEFT JOIN biobank_container_parent bc + // ON bc.ContainerID=bcp.ParentContainerID + // LEFT JOIN biobank_container bc2 + // ON bcp.ContainerID=bc2.ContainerID + // WHERE bc.ContainerID=:i'; + // $childContainerBarcodes = $this->db->pselectColWithIndexKey( + // $query, + // ['i' => $id], + // 'Coordinate' + // ); + + // return $childContainerBarcodes; + // } /** * Returns an array of all the Project IDs associated with the given @@ -146,7 +154,9 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance bc.Temperature, bc.CenterID, bcp.ParentContainerID, - GROUP_CONCAT(DISTINCT(bcp2.ContainerID)) as ChildContainerIDs, + bc2.Barcode as ParentContainerBarcode, + GROUP_CONCAT(DISTINCT bcp2.ContainerID ORDER BY bcp2.ContainerID) as ChildContainerIDs, + GROUP_CONCAT(DISTINCT bcp2.Coordinate ORDER BY bcp2.ContainerID) as Coordinates, bcp.Coordinate as Coordinate, bc.LotNumber, bc.ExpirationDate, @@ -156,10 +166,12 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance FROM biobank_container bc LEFT JOIN biobank_container_parent bcp ON bc.ContainerID=bcp.ContainerID + LEFT JOIN biobank_container bc2 + ON bcp.ParentContainerID=bc2.ContainerID LEFT JOIN biobank_container_parent bcp2 ON bc.ContainerID=bcp2.ParentContainerID LEFT JOIN biobank_container_type bct - USING (ContainerTypeID) + ON bc.ContainerTypeID=bct.ContainerTypeID LEFT JOIN biobank_container_project_rel bcpr ON bc.ContainerID=bcpr.ContainerID LEFT JOIN biobank_container_shipment_rel bcsr @@ -172,8 +184,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $whereClause = []; foreach ($conditions as $condition) { $whereClause[] = $condition['column'] - . '=' - . '"' + . '="' . $condition['value'] . '"'; } @@ -184,26 +195,16 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $containers = []; foreach ($containerRows as $id => $containerRow) { - // TODO: This whole section needs to be redone more like - // the shipment module - $childContainerIds = $containerRow['ChildContainerIDs'] - ? explode(',', $containerRow['ChildContainerIDs']) - : []; - $projectIds = explode(',', $containerRow['ProjectIDs']); - $shipmentBarcodes = $containerRow['ShipmentBarcodes'] - ? explode(',', $containerRow['ShipmentBarcodes']) - : []; - $containers[$id] = $this->_getInstanceFromSQL( + $containers[$id] = $this->_getInstanceFromSQL( $containerRow, - $childContainerIds, - $projectIds, - $shipmentBarcodes + explode(',', $containerRow['ChildContainerIDs'] ?? ''), + explode(',', $containerRow['Coordinates'] ?? ''), + array_map(fn($id) => new \ProjectID($id), explode(',', $containerRow['ProjectIDs'])), + explode(',', $containerRow['ShipmentBarcodes'] ?? '') ); } - return $containers; } - /** * Instantiates an ArrayIterator class that is composed of all the Container * Objects. @@ -342,7 +343,10 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $query = "SELECT ContainerID as childContainerId, ParentContainerID as parentContainerId, Coordinate as coordinate - FROM biobank_container_parent"; + Barcode as barcode + FROM biobank_container_parent + LEFT JOIN biobank_container + USING (ContainerID)"; $result = $this->db->pselect($query, []); $coordinates = []; foreach ($result as $row) { @@ -351,9 +355,15 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance }; $container =& $coordinates[$row['parentContainerId']]; if (empty($row['coordinate'])) { - $container['Unassigned'][] = $row['childContainerId']; + $container['Unassigned'][] = [ + 'id' => $row['childContainerId'], + 'barcode' => $row['barcode'], + ]; } else { - $container[$row['coordinate']] = $row['childContainerId']; + $container[$row['coordinate']] = [ + 'id' => $row['childContainerId'], + 'barcode' => $row['barcode'], + ]; } } @@ -461,11 +471,11 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance ); $updatedContainers = array_merge($updatedChildren, $updatedContainers); } - if ($container->getCenterID() !== $oldContainer->getCenterID()) { + if ($container->getCenterId()->__toString() !== $oldContainer->getCenterId()->__toString()) { $updatedChildren = $this->_cascadeToChildren( $container, 'CenterID', - $container->getCenterID() + $container->getCenterId()->__toString() ); $updatedContainers = array_merge($updatedChildren, $updatedContainers); } @@ -591,28 +601,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance return $childContainerIds; } - // FIXME: Not in use. Delete if not used by November. - /** - * This function takes a Container Object and retrieves an array of objects - * representing the childre of the $container. - * - * @param object $container Container Object from which to retrieve - * children - * - * @return array $childContainers Array of child container instances - */ - // public function getChildContainers(Container $container) : array - // { - // $childContainers = array(); - // $childContainerIds = $this->_getChildContainerIds($container->getId()); - // if (!empty($childContainerIds)) { - // foreach ($childContainerIds as $id) { - // $childContainers[$id] = $this->getInstanceFromId((int) $id); - // } - // } - // return $childContainers; - // } - /** * This function takes a Container Object and prepares the data to be * inserted into the database by converting it to a data array. This mapping @@ -635,7 +623,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance 'ContainerTypeID' => $container->getTypeId(), 'Temperature' => $container->getTemperature(), 'ContainerStatusID' => $container->getStatusId(), - 'CenterID' => $container->getCenterID(), + 'CenterID' => $container->getCenterId()->__toString(), 'LotNumber' => $container->getLotNumber(), 'ExpirationDate' => $expirationDate, 'Comments' => $container->getComments(), @@ -677,6 +665,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance private function _getInstanceFromSQL( array $data, ?array $childContainerIds, + ?array $coordinates, ?array $projectIds, ?array $shipmentBarcodes ) : Container { @@ -715,9 +704,15 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['ParentContainerID'])) { $container->setParentContainerId((int) $data['ParentContainerID']); } + if (isset($data['ParentContainerBarcode'])) { + $container->setParentContainerBarcode((string) $data['ParentContainerBarcode']); + } if (isset($childContainerIds)) { $container->setChildContainerIds($childContainerIds); } + if (isset($coordinates)) { + $container->setCoordinates($coordinates); + } if (isset($data['Coordinate'])) { $container->setCoordinate((int) $data['Coordinate']); } diff --git a/php/containerendpoint.class.inc b/php/containerendpoint.class.inc index 01e05d09..fb445e85 100644 --- a/php/containerendpoint.class.inc +++ b/php/containerendpoint.class.inc @@ -99,7 +99,7 @@ class ContainerEndpoint extends \NDB_Page implements RequestHandlerInterface public function handle(ServerRequestInterface $request) : ResponseInterface { ini_set('memory_limit', '-1'); - $db = $this->loris->getDatabaseConnection(); + $db = \NDB_Factory::singleton()->database(); $user = $request->getAttribute('user'); $contCont = new ContainerController($this->loris, $user); @@ -148,6 +148,12 @@ class ContainerEndpoint extends \NDB_Page implements RequestHandlerInterface return new \LORIS\Http\Response\JSON\InternalServerError( $e->getMessage() ); + } catch (\Exception $e) { + $db->rollBack(); + // PUT SOMETHING HERE LATER + return new \LORIS\Http\Response\JSON\InternalServerError( + $e->getMessage() + ); } finally { if ($db->inTransaction()) { $db->commit(); diff --git a/php/log.class.inc b/php/log.class.inc index 3f09e7e3..c0f38974 100644 --- a/php/log.class.inc +++ b/php/log.class.inc @@ -37,7 +37,7 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance * These properties describe a shipment entity. * * @var string $barcode - * @var string $centerId + * @var \CenterID $centerId * @var string $status * @var string $user * @var string $temperature @@ -45,14 +45,14 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance * @var \DateTime $time * @var string $comments */ - public $barcode; - public $centerId; - public $status; - public $user; - public $temperature; - public $date; - public $time; - public $comments; + public ?string $barcode; + public ?\CenterID $centerId; + public ?string $status; + public ?string $user; + public ?string $temperature; + public ?\DateTime $date; + public ?\DateTime $time; + public ?string $comments; /** * Initiliazes a new instance of the Log Class @@ -62,12 +62,12 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance function __construct(array $data) { $this->barcode = $data['barcode'] ?? null; - $this->centerId = $data['centerId'] ?? null; + $this->centerId = new \CenterID($data['centerId'] ?? null); $this->status = $data['status'] ?? null; $this->user = $data['user'] ?? null; $this->temperature = $data['temperature'] ?? null; - $this->date = $data['date'] ?? null; - $this->time = $data['time'] ?? null; + $this->date = new \DateTime($data['date'] ?? null); + $this->time = new \DateTime($data['time'] ?? null); $this->comments = $data['comments'] ?? null; } @@ -84,9 +84,9 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance /** * Get the center for this log * - * @return ?string + * @return \CenterID */ - public function getCenterId() : ?string + public function getCenterId() : \CenterID { return $this->centerId; } diff --git a/php/module.class.inc b/php/module.class.inc index 59fdc3b1..c2fde0a6 100644 --- a/php/module.class.inc +++ b/php/module.class.inc @@ -256,7 +256,7 @@ class Module extends \Module */ public function getMenuCategory() : string { - return "Biospecimens"; + return "Biobank"; } /** diff --git a/php/optionsendpoint.class.inc b/php/optionsendpoint.class.inc index c1aa3e94..70e7acc1 100644 --- a/php/optionsendpoint.class.inc +++ b/php/optionsendpoint.class.inc @@ -97,7 +97,7 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface public function handle(ServerRequestInterface $request) : ResponseInterface { ini_set('memory_limit', '-1'); - $db = $this->loris->getDatabaseConnection(); + $db = \NDB_Factory::singleton()->database(); $user = $request->getAttribute('user'); try { @@ -111,6 +111,10 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface } } catch (Forbidden $e) { return new \LORIS\Http\Response\JSON\Forbidden($e->getMessage()); + } catch (\Exception $e) { + return new \LORIS\Http\Response\JSON\InternalServerError( + $e->getMessage() + ); } } @@ -129,6 +133,7 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface $shipDAO = new ShipmentDAO($db); $userCenters = implode(',',$user->getCenterIDs()); + $userProjects = implode(',', $user->getProjectIDs()); // XXX: This should eventually be replaced by a call directly to a // Candidate endpoint or Candidate controller that will be able to // provide Candidate Objects. @@ -140,15 +145,13 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface LEFT JOIN session s USING (CandID) LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) LEFT JOIN biobank_container bc USING (ContainerID) - LEFT JOIN biobank_specimen_pool_rel bspr USING (SpecimenID) - LEFT JOIN biobank_pool bp USING (PoolID) LEFT JOIN candidate_diagnosis_rel USING (CandID)"; $where = ' '; - if (!$user->hasPermission('access_all_profiles')) { - $where = " WHERE s.CenterID IN ($userCenters) - OR bc.CenterID IN ($userCenters) - OR bp.CenterID IN ($userCenters)"; - } + // XXX: This is commented out because the access all profiles permission + // is causing some issues. + // if (!$user->hasPermission('access_all_profiles')) { + $where = " WHERE s.CenterID IN ($userCenters) OR s.ProjectID IN ($userProjects)"; + // } $group = " GROUP BY CandID"; $candidates = $db->pselectWithIndexKey($query.$where.$group, array(), 'id'); foreach ($candidates as $id => $candidate) { @@ -202,14 +205,11 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface FROM candidate c LEFT JOIN session s USING(CandID) LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) - LEFT JOIN biobank_container bc USING (ContainerID) - LEFT JOIN biobank_specimen_pool_rel bspr USING (SpecimenID) - LEFT JOIN biobank_pool bp USING (PoolID)"; + LEFT JOIN biobank_container bc USING (ContainerID)"; $where = ' '; if (!$user->hasPermission('access_all_profiles')) { $where = " WHERE s.CenterID IN ($userCenters) - OR bc.CenterID IN ($userCenters) - OR bp.CenterID IN ($userCenters)"; + OR bc.CenterID IN ($userCenters)"; } $result = $db->pselect($query.$where, array()); $candidateSessions = []; diff --git a/php/pool.class.inc b/php/pool.class.inc index 41d06f21..bafa3154 100644 --- a/php/pool.class.inc +++ b/php/pool.class.inc @@ -29,7 +29,10 @@ namespace LORIS\biobank; * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 * @link https://www.github.com/aces/Loris/ */ -class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyEntities\SiteHaver +class Pool implements + \JsonSerializable, + \LORIS\Data\DataInstance, + \LORIS\StudyEntities\SiteHaver { /** * Persistent Instance variables. @@ -41,10 +44,13 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE * @var float $quantity * @var int $unitId * @var array $specimenIds + * @var array $specimenBarcodes * @var int $candidateId + * @var string $candidatePSCID * @var int $sessionId * @var int $typeId * @var \CenterID $centerId + * @var array $projectIds * @var \DateTime $date * @var \DateTime $time */ @@ -53,10 +59,13 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE private $quantity; private $unitId; private $specimenIds; + private array $specimenBarcodes; private $candidateId; + private string $candidatePSCID; private $sessionId; private $typeId; - private \CenterID $centerId; + private $centerId; + private $projectIds; private $date; private $time; @@ -181,6 +190,28 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE return $this->specimenIds; } + /** + * Sets the Specimen Barcodes of the pool + * + * @param array $specimenBarcodes Specimen Barcodes of the pool + * + * @return void + */ + public function setSpecimenBarcodes(array $specimenBarcodes) : void + { + $this->specimenBarcodes = $specimenBarcodes; + } + + /** + * Gets the Specimen Barcodess of the pool + * + * @return array + */ + public function getSpecimenBarcodes() : array + { + return $this->specimenBarcodes; + } + /** * Sets the Candidate ID of the pool * @@ -205,6 +236,28 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE return $this->candidateId; } + /** + * Sets the Candidate PSCID of the pool + * + * @param int $candidateId Candidate ID of the pool + * + * @return void + */ + public function setCandidatePSCID(string $candidatePSCID) : void + { + $this->candidatePSCID = $candidatePSCID; + } + + /** + * Gets the Candidate PSCID of the pool + * + * @return string + */ + public function getCandidatePSCID() : string + { + return $this->candidatePSCID; + } + /** * Sets the Session ID of the pool * @@ -270,11 +323,32 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE * * @return \CenterID */ - public function getCenterID() : \CenterID + public function getCenterId() : \CenterID { return $this->centerId; } + /** + * Sets the Project IDs of the constituent specimens + * + * @param array $projectIds Project IDs of the pool + * + * @return void + */ + public function setProjectIds(array $projectIds) : void + { + $this->projectIds = $projectIds; + } + + /** + * Gets the Project IDs of the pool + * + * @return array + */ + public function getProjectIds() : array + { + return $this->projectIds; + } /** * Sets the date at which the pool was created. * @@ -343,9 +417,15 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE if (isset($data['specimenIds'])) { $this->setSpecimenIds($data['specimenIds']); } + if (isset($data['specimenBarcodes'])) { + $this->setSpecimenBarcodes($data['specimenBarcodes']); + } if (isset($data['candidateId'])) { $this->setCandidateId((int) $data['candidateId']); } + if (isset($data['candidatePSCID'])) { + $this->setCandidatePSCID((string) $data['candidatePSCID']); + } if (isset($data['sessionId'])) { $this->setSessionId((int) $data['sessionId']); } @@ -355,6 +435,12 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE if (isset($data['centerId'])) { $this->setCenterId(new \CenterID(strval($data['centerId']))); } + if (isset($data['projectIds'])) { + $this->setProjectIds($data['projectIds']); + } + if (isset($data['projectIds'])) { + $this->setProjectIds($data['projectIds']); + } if (isset($data['date'])) { $this->setDate(new \DateTime($data['date'])); } @@ -386,17 +472,20 @@ class Pool implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyE // TODO: Date and Time formats should be gotten from some sort of // config setting. return [ - 'id' => $this->id, - 'label' => $this->label, - 'quantity' => $this->quantity, - 'unitId' => $this->unitId, - 'specimenIds' => $this->specimenIds, - 'candidateId' => $this->candidateId, - 'sessionId' => $this->sessionId, - 'typeId' => $this->typeId, - 'centerId' => $this->centerId->__toString(), - 'date' => $this->date->format('d-m-Y'), - 'time' => $this->time->format('H:i'), + 'id' => $this->id, + 'label' => $this->label, + 'quantity' => $this->quantity, + 'unitId' => $this->unitId, + 'specimenIds' => $this->specimenIds, + 'specimenBarcodes' => $this->specimenBarcodes, + 'candidateId' => $this->candidateId, + 'candidatePSCID' => $this->candidatePSCID, + 'sessionId' => $this->sessionId, + 'typeId' => $this->typeId, + 'centerId' => $this->centerId, + 'projectIds' => $this->projectIds, + 'date' => $this->date->format('d-m-Y'), + 'time' => $this->time->format('H:i'), ]; } diff --git a/php/poolcontroller.class.inc b/php/poolcontroller.class.inc index c10f4b74..a2d93714 100644 --- a/php/poolcontroller.class.inc +++ b/php/poolcontroller.class.inc @@ -97,9 +97,8 @@ class PoolController private function _getDataProvisioner() : \LORIS\Data\Provisioner { $dao = new PoolDAO($this->loris); - if ($this->user->hasPermission('access_all_profiles') == false) { - $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - } + $dao = $dao + ->filter(new \LORIS\Data\Filters\UserProjectOrSiteMatch()); return $dao; } @@ -139,7 +138,6 @@ class PoolController // $this->_validateQuantity($pool); // $this->_validateUnitId($pool); $this->_validateSpecimenIds($pool); - $this->_validateCenterId($pool); // $this->_validateDate($pool); // $this->_validateTime($pool); } @@ -185,8 +183,10 @@ class PoolController ); } + // XXX: not sure why a loris oobject is being passed to instatiate the + // DAO. Maybe this is right, but I'm not sure why. $specimenDAO = new SpecimenDAO($this->loris); - $containerDAO = new ContainerDAO($this->loris->getDatabaseConnection()); + $containerDAO = new ContainerDAO($this->db); $baseSpecimen = $specimenDAO->getInstanceFromId($specimenIds[0]); $baseContainer = $containerDAO->getInstanceFromId( $baseSpecimen->getContainerId() @@ -215,7 +215,7 @@ class PoolController throw new BadRequest('Pooled specimens must be of the same Type'); } - if ($baseContainer->getCenterID() != $container->getCenterID()) { + if ($baseContainer->getCenterId()->__toString() !== $container->getCenterId()->__toString()) { throw new BadRequest('Pooled specimens must be at the same Site'); } @@ -238,20 +238,4 @@ class PoolController } } } - - /** - * Validates Pool Object Center ID - * - * @param Pool $pool Pool to be checked. - * - * @throws BadRequest if the provided Pool does not meet validation requirements - * - * @return void - */ - private function _validateCenterId(Pool $pool) : void - { - if (is_null($pool->getCenterID())) { - throw new BadRequest('Pool Center must be set'); - } - } } diff --git a/php/pooldao.class.inc b/php/pooldao.class.inc index 8ae0877b..721b0ae5 100644 --- a/php/pooldao.class.inc +++ b/php/pooldao.class.inc @@ -88,7 +88,6 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance bp.Quantity, bp.UnitID, GROUP_CONCAT(bspr.SpecimenID) as SpecimenIDs, - bp.CenterID, bp.Date, DATE_FORMAT(bp.Time, '%H:%i') as Time FROM biobank_pool bp @@ -110,21 +109,32 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance $pools = []; $specimenDAO = new SpecimenDAO($this->loris); - if (!empty($poolRows)) { - foreach ($poolRows as $id => $poolRow) { - $specimenIds = explode(',', $poolRow['SpecimenIDs']); - $specimenId = (int) $specimenIds[0]; - $specimen = $specimenDAO->selectInstances( - [ - ['column'=>'SpecimenID', 'value'=>$specimenId], - ], - )[$specimenId]; - $poolRow['CandidateID'] = $specimen->getCandidateId(); - $poolRow['SessionID'] = $specimen->getSessionId(); - $poolRow['TypeID'] = $specimen->getTypeId(); - - $pools[$id] = $this->_getInstanceFromSQL($poolRow, $specimenIds); + + foreach ($poolRows as $id => $poolRow) { + $specimenIds = explode(',', $poolRow['SpecimenIDs']); + $specimenBarcodes = []; + foreach ($specimenIds as $specimenId) { + $specimen = $specimenDAO->getInstanceFromId((int) $specimenId); + $specimenBarcodes[] = $specimen->getBarcode(); } + + // set global pool properties based on first specimen of pool + $specimenId = (int) $specimenIds[0]; + $specimen = $specimenDAO->getInstanceFromId($specimenId); + $poolRow['CandidateID'] = $specimen->getCandidateId(); + $poolRow['CandidatePSCID'] = $specimen->getCandidatePSCID(); + $poolRow['SessionID'] = $specimen->getSessionId(); + $poolRow['CenterID'] = $specimen->getCenterId(); + $projectIds = $specimen->getProjectIds(); + $poolRow['TypeID'] = $specimen->getTypeId(); + + // instantiate pool entity + $pools[$id] = $this->_getInstanceFromSQL( + $poolRow, + $specimenIds, + $specimenBarcodes, + $projectIds + ); } return $pools; @@ -206,7 +216,6 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance 'Label' => $pool->getLabel(), 'Quantity' => $pool->getQuantity(), 'UnitID' => $pool->getUnitId(), - 'CenterID' => $pool->getCenterID(), 'Date' => $pool->getDate()->format('Y-m-d'), 'Time' => $pool->getTime()->format('H:i'), ]; @@ -232,10 +241,17 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance * @param array $data Values to be reassigned. * @param array $specimenIds List of specimen IDs associated with the given * Pool. + * @param array $projectIds List of project IDs associated with the given + * Pool via the constituent specimens. * * @return Pool */ - private function _getInstanceFromSQL(array $data, array $specimenIds) : Pool + private function _getInstanceFromSQL( + array $data, + array $specimenIds, + array $specimenBarcodes, + array $projectIds + ) : Pool { $pool = new Pool(); if (isset($data['PoolID'])) { @@ -253,17 +269,31 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance if (!empty($specimenIds)) { $pool->setSpecimenIds($specimenIds); } + if (!empty($specimenBarcodes)) { + $pool->setSpecimenBarcodes($specimenBarcodes); + } if (isset($data['CandidateID'])) { $pool->setCandidateId((int) $data['CandidateID']); } + if (isset($data['CandidatePSCID'])) { + $pool->setCandidatePSCID((string) $data['CandidatePSCID']); + } + if (isset($data['SessionID'])) { + $pool->setSessionId((int) $data['SessionID']); + } if (isset($data['SessionID'])) { $pool->setSessionId((int) $data['SessionID']); } if (isset($data['TypeID'])) { $pool->setTypeId((int) $data['TypeID']); } + if (isset($projectIds)) { + $pool->setProjectIds($projectIds); if (isset($data['CenterID'])) { - $pool->setCenterId(new \CenterID(strval($data['CenterID']))); + $pool->setCenterId($data['CenterID']); + } + if (isset($projectIds)) { + $pool->setProjectIds($projectIds); } if (isset($data['Date'])) { $pool->setDate(new \DateTime($data['Date'])); diff --git a/php/poolendpoint.class.inc b/php/poolendpoint.class.inc index bfbe85e5..573c84c3 100644 --- a/php/poolendpoint.class.inc +++ b/php/poolendpoint.class.inc @@ -97,7 +97,7 @@ class PoolEndpoint extends \NDB_Page implements RequestHandlerInterface */ public function handle(ServerRequestInterface $request) : ResponseInterface { - $db = $this->loris->getDatabaseConnection(); + $db = \NDB_Factory::singleton()->database(); $user = $request->getAttribute('user'); $poolCont = new PoolController($this->loris, $user); @@ -131,6 +131,12 @@ class PoolEndpoint extends \NDB_Page implements RequestHandlerInterface return new \LORIS\Http\Response\JSON\InternalServerError( $e->getMessage() ); + } catch (\Exception $e) { + $db->rollBack(); + // PUT SOMETHING HERE LATER + return new \LORIS\Http\Response\JSON\InternalServerError( + $e->getMessage() + ); } finally { if ($db->inTransaction()) { $db->commit(); diff --git a/php/shipment.class.inc b/php/shipment.class.inc index 11dde717..b48d1310 100644 --- a/php/shipment.class.inc +++ b/php/shipment.class.inc @@ -29,26 +29,31 @@ namespace LORIS\biobank; * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 * @link https://www.github.com/aces/Loris/ */ -class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance +class Shipment implements + \JsonSerializable, + \LORIS\Data\DataInstance, + \LORIS\StudyEntities\MultiSiteHaver { /** * Persistent Instance variables. * * These properties describe a shipment entity. * - * @var string $id - * @var string $barcode - * @var string $type - * @var string $destinationCenterId - * @var array $containerIds - * @var array $logs + * @var string $id + * @var string $barcode + * @var string $type + * @var \CenterID $destinationCenterId + * @var array $containerIds + * @var array $containerBarcodes + * @var array $logs */ - public ?string $id; - public ?string $barcode; - public ?string $type; - public ?string $destinationCenterId; - public ?array $containerIds; - public ?array $logs; + public ?string $id; + public ?string $barcode; + public ?string $type; + public ?\CenterID $destinationCenterId; + public ?array $containerIds; + public ?array $containerBarcodes; + public ?array $logs; /** * Initiliazes a new instance of the Shipment Class @@ -60,8 +65,9 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance $this->id = $data['id'] ?? null; $this->barcode = $data['barcode'] ?? null; $this->type = $data['type'] ?? null; - $this->destinationCenterId = $data['destinationCenterId'] ?? null; + $this->destinationCenterId = new \CenterID($data['destinationCenterId'] ?? null); $this->containerIds = $data['containerIds'] ?? null; + $this->containerBarcodes = $data['containerBarcodes'] ?? null; $this->_setLogs($data['logs']) ?? null; } @@ -110,9 +116,9 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance /** * Gets the origin center of the shipment * - * @return ?string + * @return \CenterID */ - public function getOriginCenterId() : ?string + public function getOriginCenterId() : \CenterID { if (!empty($this->getLogs())) { return $this->getLogs()[0]->centerId; @@ -122,9 +128,9 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance /** * Gets the destination center of the shipment * - * @return ?string + * @return \CenterID */ - public function getDestinationCenterId() : ?string + public function getDestinationCenterId() : \CenterID { return $this->destinationCenterId; } @@ -162,6 +168,16 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance return $this->containerIds; } + /** + * Gets the container barcodes associated with the shipment + * + * @return ?array + */ + public function getContainerBarcodes() : ?array + { + return $this->containerBarcodes; + } + /** * Sets the logs for the shipment * @@ -171,7 +187,6 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance */ private function _setLogs(array $logs) : void { - // TODO: THis should maybe go in the SHipment Handler. foreach ($logs as $i => $log) { $this->logs[$i] = new Log($log); } @@ -190,15 +205,22 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance /** * Return Center ID of affiliated specimen container. * - * @return int + * @return array */ - // NOTE: this function was added for reasons relating to permissions. I do - // find this an ideal solution, given that the centerId and the - // destinationCenterId should not be equated. Rather, this should - // ideally return a value dependent on the current status of the shipment - public function getCenterId() + public function getCenterIds() : array { - return $this->getDestinationCenterId(); + switch ($this->getStatus()) { + // TODO: Instead of checking for strings, this ideally would check for + // boolean values from functions such as getActive(). This way, the + // strings will be relegated to singular functions that then serve + // functional purposes. + case 'received': + return [$this->getDestinationCenterId()]; + case 'shipped': + return [$this->getOriginCenterId(), $this->getDestinationCenterId]; + default: + return [$this->getOriginCenterId()]; + } } /** @@ -226,6 +248,7 @@ class Shipment implements \JsonSerializable, \LORIS\Data\DataInstance 'originCenterId' => $this->getOriginCenterId(), 'destinationCenterId' => $this->destinationCenterId, 'containerIds' => $this->containerIds, + 'containerBarcodes' => $this->containerBarcodes, 'status' => $this->getStatus(), 'active' => $this->getActive(), 'logs' => $this->logs, diff --git a/php/shipmentdao.class.inc b/php/shipmentdao.class.inc index 1a6eb8d8..63233c88 100644 --- a/php/shipmentdao.class.inc +++ b/php/shipmentdao.class.inc @@ -97,7 +97,8 @@ class ShipmentDAO extends \LORIS\Data\ProvisionerInstance s.Barcode as barcode, st.Label as type, s.DestinationCenterID as destinationCenterId, - GROUP_CONCAT(bc.ContainerId) as containerIds + GROUP_CONCAT(bc.ContainerId) as containerIds, + GROUP_CONCAT(bc.Barcode) as containerBarcodes FROM shipment s LEFT JOIN shipment_type st USING (ShipmentTypeID) @@ -126,6 +127,8 @@ class ShipmentDAO extends \LORIS\Data\ProvisionerInstance foreach ($shipmentRows as $barcode => $shipmentRow) { $shipmentRow['containerIds'] = $shipmentRow['containerIds'] ? explode(',', $shipmentRow['containerIds']) : []; + $shipmentRow['containerBarcodes'] = $shipmentRow['containerBarcodes'] ? + explode(',', $shipmentRow['containerBarcodes']) : []; $shipmentRow['logs'] = $logs[$barcode]; $shipment = new Shipment($shipmentRow); $shipments[$barcode] = $shipment; diff --git a/php/shipmenthandler.class.inc b/php/shipmenthandler.class.inc index f04b70ac..dcc034d1 100644 --- a/php/shipmenthandler.class.inc +++ b/php/shipmenthandler.class.inc @@ -55,7 +55,7 @@ class ShipmentHandler { $this->db = $db; $this->user = $user; - $this->dao = $this->_getDataProvisioner(); + $this->dao = new ShipmentDAO($this->db); $this->shipment = new Shipment($data); $this->_validate($this->shipment); } @@ -86,22 +86,6 @@ class ShipmentHandler return $this->dao->saveInstance($this->shipment); } - /** - * Treats the Shipment DAO as a Provisioner that can be iteratated - * through to provide the permissable Shipment Objects for the current User. - * - * @return \LORIS\Data\Provisioner - */ - private function _getDataProvisioner() : \LORIS\Data\Provisioner - { - $dao = new ShipmentDAO($this->db); - - if ($this->user->hasPermission('access_all_profiles') === false) { - $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - } - return $dao; - } - /** * Checks User Permissions for creating or updating Shipment Objects. * @@ -160,7 +144,7 @@ class ShipmentHandler $errors = []; $errors = $this->_validateBarcode($shipment, $errors); $errors = $this->_validateType($shipment, $errors); - $errors = $this->_validateDestinationCenterId($shipment, $errors); + $errors = $this->_validateDestinationCenter($shipment, $errors); $errors = $this->_validateLogs($shipment, $errors); $errors = $this->_validateContainers($shipment, $errors); $this->errors = $errors; @@ -221,7 +205,7 @@ class ShipmentHandler * * @return array */ - private function _validateDestinationCenterId( + private function _validateDestinationCenter( Shipment $shipment, array $errors ) : array { @@ -233,7 +217,7 @@ class ShipmentHandler // TODO: Check that Destination Center is an actual center. if (!empty($error)) { - $errors['destinationCenterId'] = $error; + $errors['destinationCenter'] = $error; } return $errors; } diff --git a/php/shipments.class.inc b/php/shipments.class.inc index 296bdba7..6954147c 100644 --- a/php/shipments.class.inc +++ b/php/shipments.class.inc @@ -1,6 +1,6 @@ loris = $request->getAttribute('loris'); - $this->db = $this->loris->getDatabaseConnection(); + $this->db = \NDB_Factory::singleton()->database(); $this->user = $request->getAttribute('user'); - // TODO: This entire thing NEEDS to have a try block wrapped around it! try { $this->db->beginTransaction(); switch ($request->getMethod()) { @@ -121,6 +119,12 @@ class Shipments implements RequestHandlerInterface return (new \LORIS\Http\Response()) ->withHeader('Allow', $this->allowedMethods()); } + } catch (\Exception $e) { + $this->db->rollBack(); + // PUT SOMETHING HERE LATER + return new \LORIS\Http\Response\JSON\InternalServerError( + $e->getMessage() + ); } finally { if ($this->db->inTransaction()) { $this->db->commit(); @@ -135,15 +139,12 @@ class Shipments implements RequestHandlerInterface */ private function _handleGET() : ResponseInterface { - // TODO: This needs to be checked but should probably go somewhere else... // if (!$this->user->hasPermission('biobank_shipment_view')) { - // throw new \Forbidden('Shipment: View Permission Denied'); + // return new \Forbidden('Shipment: View Permission Denied'); // } $shipments = []; - $dao = new ShipmentDAO($this->db); - if ($this->user->hasPermission('access_all_profiles') === false) { - $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - } + $dao = (new ShipmentDAO($this->db)) + ->filter(new \LORIS\Data\Filters\UserSiteMatch()); $shipmentIterator = $dao->execute($this->user); foreach ($shipmentIterator as $barcode=>$shipment) { $shipments[$barcode] = $shipment; @@ -167,8 +168,6 @@ class Shipments implements RequestHandlerInterface $this->user, ); - // TODO: A lot of what the handler does should ACTUALLY be done here. - // Handlers should not know how operate multiple shipments, only 1 each! if (!$shipmentHandler->isValid()) { return new \LORIS\Http\Response\JsonResponse( $shipmentHandler->getErrors(), @@ -199,8 +198,6 @@ class Shipments implements RequestHandlerInterface $this->user, ); - // TODO: A lot of what the handler does should ACTUALLY be done here. - // Handlers should not know how operate multiple shipments, only 1 each! if (!$shipmentHandler->isValid()) { return new \LORIS\Http\Response\JsonResponse( $shipmentHandler->getErrors(), @@ -209,7 +206,7 @@ class Shipments implements RequestHandlerInterface } return new \LORIS\Http\Response\JsonResponse( - $shipmentHandler->createInstance(), + $shipmentHandler->updateInstance(), 200, ); } diff --git a/php/specimen.class.inc b/php/specimen.class.inc index a5f8a506..74aa7f1d 100644 --- a/php/specimen.class.inc +++ b/php/specimen.class.inc @@ -29,7 +29,10 @@ namespace LORIS\biobank; * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 * @link https://www.github.com/aces/Loris/ */ -class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\StudyEntities\SiteHaver +class Specimen implements + \JsonSerializable, + \LORIS\Data\DataInstance, + \LORIS\StudyEntities\SiteHaver { /** * Persistent Instance variables. @@ -38,13 +41,17 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St * * @var int $id * @var int $containerId + * @var string $barcode * @var int $typeId * @var string $quantity * @var int $unitId - * @var \CenterID $containerCenterId + * @var \CenterID $CenterId + * @var array $ProjectIds * @var int $fTCycle * @var array $parentSpecimenIds + * @var array $parentSpecimenBarcodes * @var int $candidateId + * @var string $candidatePSCID * @var int $candidateAge * @var int $sessionId * @var int $poolId @@ -54,13 +61,17 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St */ private $id; private $containerId; + private string $barcode; private $typeId; private $quantity; private $unitId; private $fTCycle; - private \CenterID $containerCenterId; + private $centerId; + private $projectIds; private $parentSpecimenIds; + private $parentSpecimenBarcodes; private $candidateId; + private $candidatePCSID; private $candidateAge; private $sessionId; private $poolId; @@ -68,17 +79,6 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St private $preparation; private $analysis; - private $loris; - /** - * Initiliazes a new instance of the Specimen Class - * - * @param \LORIS\LorisInstance $loris The LORIS instance with the specimen - */ - function __construct(\LORIS\LorisInstance $loris) - { - $this->loris = $loris; - } - /** * Sets the specimen's ID. * @@ -127,6 +127,28 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St return $this->containerId; } + /** + * Sets the specimen's barcode + * + * @param string $barcode + * + * @return void + */ + public function setBarcode($barcode) : void + { + $this->barcode = $barcode; + } + + /** + * Gets the specimen's barcode + * + * @return string + */ + public function getBarcode() : string + { + return $this->barcode; + } + /** * Sets the ID of the specimen type * @@ -243,6 +265,28 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St return $this->parentSpecimenIds; } + /** + * Sets the parent specimen's barcodes. + * + * @param array $parentSpecimenBarocdes the parent specimen's Barcodes + * + * @return void + */ + public function setParentSpecimenBarcodes(array $parentSpecimenBarcodes) : void + { + $this->parentSpecimenBarcodes = $parentSpecimenBarcodes; + } + + /** + * Gets the parent specimen's barcodes. + * + * @return ?array + */ + public function getParentSpecimenBarcodes() : ?array + { + return $this->parentSpecimenBarcodes; + } + /** * Sets the ID of the candidate to which the specimen belongs * @@ -267,6 +311,28 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St return $this->candidateId; } + /** + * Sets the PSCID of the candidate to which the specimen belongs + * + * @param string $pscid of the specimen's donor + * + * @return void + */ + public function setCandidatePSCID(string $candidatePSCID) : void + { + $this->candidatePSCID = $candidatePSCID; + } + + /** + * Gets the PSCID of the candidate to which the specimen belongs + * + * @return string + */ + public function getCandidatePSCID() : string + { + return $this->candidatePSCID; + } + /** * Sets the Age of the candidate when the specimen was created * @@ -408,34 +474,34 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St return $this->analysis; } - public function setContainerCenterId(\CenterID $containerCenterId) + public function setCenterId(\CenterID $centerId) { - $this->containerCenterId = $containerCenterId; + $this->centerId = $centerId; } - // FIXME: THIS IS A MASSIVE HACK. Specimens should not be provisioned if - // their Container is not provisioned. Therefore, a check must be made that - // the Container's Center ID is accesibile by the current User. This function - // allows this check to happen upon provisioner filtering. /** * Return Center ID of affiliated specimen container. * * @return \CenterID */ - public function getCenterID() : \CenterID + public function getCenterId() : \CenterID { - // NOTE: with the introduction of setContainerCenterId, this has been - // temporarily disabled to fix performance issues. A more permanent - // fix should be issued that considers container and specimen object - // design + return $this->centerId; + } - // $db = \Database::singleton(); - // $containerDAO = new ContainerDAO($db); - // $containerId = $this->getContainerId(); - // $container = $containerDAO->getInstanceFromId($containerId); - // return $container->getCenterId(); + public function setProjectIds(array $projectIds) + { + $this->projectIds = $projectIds; + } - return $this->containerCenterId; + /** + * Return Project ID of affiliated specimen container. + * + * @return \ProjectID[] + */ + public function getProjectIds() : array + { + return $this->projectIds; } /** @@ -453,6 +519,9 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St if (isset($data['containerId'])) { $this->setContainerId((int) $data['containerId']); } + if (isset($data['barcode'])) { + $this->setBarcode((string) $data['barcode']); + } if (isset($data['typeId'])) { $this->setTypeId((int) $data['typeId']); } @@ -464,6 +533,11 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St } if (isset($data['containerCenterId'])) { $this->setContainerCenterId(new \CenterID(strval($data['containerCenterId']))); + if (isset($data['projectIds'])) { + $this->setProjectIds($data['projectIds']); + } + if (isset($data['projectIds'])) { + $this->setProjectIds($data['projectIds']); } if (isset($data['fTCycle'])) { $this->setFTCycle((int) $data['fTCycle']); @@ -471,9 +545,15 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St if (isset($data['parentSpecimenIds'])) { $this->setParentSpecimenIds($data['parentSpecimenIds']); } + if (isset($data['parentSpecimenBarocdes'])) { + $this->setParentSpecimenBarcodes($data['parentSpecimenBarcodes']); + } if (isset($data['candidateId'])) { $this->setCandidateId((int) $data['candidateId']); } + if (isset($data['candidatePSCID'])) { + $this->setCandidatePSCID((string) $data['candidatePSCID']); + } if (isset($data['candidateAge'])) { $this->setCandidateAge((int) $data['candidateAge']); } @@ -523,19 +603,23 @@ class Specimen implements \JsonSerializable, \LORIS\Data\DataInstance, \LORIS\St public function jsonSerialize() : array { $array = [ - 'id' => $this->id, - 'containerId' => $this->containerId, - 'typeId' => $this->typeId, - 'quantity' => $this->quantity, - 'unitId' => $this->unitId, - 'containerCenterId' => $this->containerCenterId->__toString(), - 'fTCycle' => $this->fTCycle, - 'parentSpecimenIds' => $this->parentSpecimenIds, - 'candidateId' => $this->candidateId, - 'candidateAge' => $this->candidateAge, - 'sessionId' => $this->sessionId, - 'poolId' => $this->poolId, - 'collection' => $this->collection->jsonSerialize(), + 'id' => $this->id, + 'containerId' => $this->containerId, + 'barcode' => $this->barcode, + 'typeId' => $this->typeId, + 'quantity' => $this->quantity, + 'unitId' => $this->unitId, + 'centerId' => $this->centerId, + 'projectIds' => $this->projectIds, + 'fTCycle' => $this->fTCycle, + 'parentSpecimenIds' => $this->parentSpecimenIds, + 'parentSpecimenBarcodes' => $this->parentSpecimenBarcodes, + 'candidateId' => $this->candidateId, + 'candidatePSCID' => $this->candidatePSCID, + 'candidateAge' => $this->candidateAge, + 'sessionId' => $this->sessionId, + 'poolId' => $this->poolId, + 'collection' => $this->collection->jsonSerialize(), ]; if ($this->preparation) { $array['preparation'] = $this->preparation->jsonSerialize(); diff --git a/php/specimencontroller.class.inc b/php/specimencontroller.class.inc index fcc18645..973fd71b 100644 --- a/php/specimencontroller.class.inc +++ b/php/specimencontroller.class.inc @@ -65,6 +65,8 @@ class SpecimenController { $this->_validatePermission('view'); $specimens = []; + $this->dao = $this->dao + ->filter(new \LORIS\Data\Filters\UserProjectOrSiteMatch()); $specimenIt = $this->dao->execute($this->user); foreach ($specimenIt as $id => $specimen) { $specimens[$id] = $specimen; diff --git a/php/specimendao.class.inc b/php/specimendao.class.inc index 49b9949e..9c48fc8d 100644 --- a/php/specimendao.class.inc +++ b/php/specimendao.class.inc @@ -99,13 +99,16 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance ) : array { $query = "SELECT bs.SpecimenID, bs.ContainerID, + bc.Barcode, bs.SpecimenTypeID, bs.Quantity, bs.UnitID, - bc.CenterID as ContainerCenterID, + bc.CenterID, + GROUP_CONCAT(DISTINCT(bcpr.ProjectID)) as ProjectIDs, bsf.FreezeThawCycle, GROUP_CONCAT(bspa.ParentSpecimenID) as ParentSpecimenIDs, s.CandID as CandidateID, + c.PSCID as CandidatePSCID, bs.SessionID, ( SELECT FLOOR(DATEDIFF(s.Date_visit, c.DoB)/365.25) @@ -136,6 +139,8 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance bsa.Data as AnalysisData FROM biobank_specimen bs JOIN biobank_container bc USING (ContainerID) + LEFT JOIN biobank_container_project_rel bcpr + ON bc.ContainerID=bcpr.ContainerID LEFT JOIN biobank_specimen_freezethaw bsf USING (SpecimenID) LEFT JOIN session s @@ -172,10 +177,25 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance $parentSpecimenIds = $specimenRow['ParentSpecimenIDs'] ? explode(',', $specimenRow['ParentSpecimenIDs']) : []; + $parentSpecimenBarcodes = []; + foreach ($parentSpecimenIds as $parentSpecimenId) { + $parentSpecimen = $this->getInstanceFromId((int) $parentSpecimenId); + $parentsSpecimenBarcodes[] = $parentSpecimen->getBarcode(); + } + + $projectIds = array_map(function($projectId) { + return new \ProjectID($projectId); // store projectId within object + }, explode(',', $specimenRow['ProjectIDs'])); + + $projectIds = array_map(function($projectId) { + return new \ProjectID($projectId); // store projectId within object + }, explode(',', $specimenRow['ProjectIDs'])); $specimen = $this->_getInstanceFromSQL( $specimenRow, - $parentSpecimenIds + $projectIds, + $parentSpecimenIds, + $parentSpecimenBarcodes, ); $specimens[$id] = $specimen; @@ -625,7 +645,9 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance */ private function _getInstanceFromSQL( array $data, - array $parentSpecimenIds + array $projectIds, + array $parentSpecimenIds, + array $parentSpecimenBarcodes, ) : Specimen { $specimen = new Specimen($this->loris); if (isset($data['SpecimenID'])) { @@ -634,19 +656,23 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['ContainerID'])) { $specimen->setContainerId((int) $data['ContainerID']); } + if (isset($data['Barcode'])) { + $specimen->setBarcode((string) $data['Barcode']); + } if (isset($data['SpecimenTypeID'])) { $specimen->setTypeId((int) $data['SpecimenTypeID']); } - if (isset($data['Quantity'])) { $specimen->setQuantity((string) $data['Quantity']); } - if (isset($data['UnitID'])) { $specimen->setUnitId((int) $data['UnitID']); } - if (isset($data['ContainerCenterID'])) { - $specimen->setContainerCenterId(new \CenterID(strval($data['ContainerCenterID']))); + if (isset($data['CenterID'])) { + $specimen->setCenterId(new \CenterID($data['CenterID'])); + } + if (isset($projectIds)) { + $specimen->setProjectIds($projectIds); } if (isset($data['FreezeThawCycle'])) { $specimen->setFTCycle((int) $data['FreezeThawCycle']); @@ -654,13 +680,18 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance if (!empty($parentSpecimenIds)) { $specimen->setParentSpecimenIds($parentSpecimenIds); + $specimen->setParentSpecimenBarcodes($parentSpecimenBarcodes); } else { $specimen->setParentSpecimenIds([]); + $specimen->setParentSpecimenBarcodes([]); } if (isset($data['CandidateID'])) { $specimen->setCandidateId((int) $data['CandidateID']); } + if (isset($data['CandidatePSCID'])) { + $specimen->setCandidatePSCID((string) $data['CandidatePSCID']); + } if (isset($data['CandidateAge'])) { $specimen->setCandidateAge((int) $data['CandidateAge']); diff --git a/php/specimenendpoint.class.inc b/php/specimenendpoint.class.inc index 8dc8fead..cbea05b5 100644 --- a/php/specimenendpoint.class.inc +++ b/php/specimenendpoint.class.inc @@ -99,7 +99,7 @@ class SpecimenEndpoint extends \NDB_Page implements RequestHandlerInterface public function handle(ServerRequestInterface $request) : ResponseInterface { ini_set('memory_limit', '-1'); - $db = $this->loris->getDatabaseConnection(); + $db = \NDB_Factory::singleton()->database(); $user = $request->getAttribute('user'); $specCont = new SpecimenController($this->loris, $user); From 92a5c0a4619bbf95754abfadcf4411380e074837 Mon Sep 17 00:00:00 2001 From: Rida Abou-Haidar Date: Thu, 10 Aug 2023 11:29:12 -0400 Subject: [PATCH 02/19] fixed loading issue and properly transitioned projects from containers to specimens --- css/biobank.css | 2 +- jsx/batchEditForm.js | 6 +- jsx/biobankIndex.js | 26 ++- jsx/container.js | 11 +- jsx/containerDisplay.js | 75 ++++---- jsx/containerForm.js | 11 -- jsx/containerParentForm.js | 5 +- jsx/containerTab.js | 10 -- jsx/globals.js | 20 +-- jsx/specimenForm.js | 2 +- jsx/specimenTab.js | 2 +- php/container.class.inc | 66 +------ php/containercontroller.class.inc | 14 +- php/containerdao.class.inc | 170 ++++-------------- php/optionsendpoint.class.inc | 42 ++--- php/pool.class.inc | 7 +- php/poolcontroller.class.inc | 2 +- php/pooldao.class.inc | 2 - php/specimen.class.inc | 8 +- php/specimendao.class.inc | 154 ++++++++++------ ...nsferProjectsfromContainerstoSpecimens.sql | 28 +++ 21 files changed, 269 insertions(+), 394 deletions(-) create mode 100644 sql/2023-08-10_DropCenterfromPoolandTransferProjectsfromContainerstoSpecimens.sql diff --git a/css/biobank.css b/css/biobank.css index 2ee31e6b..bbb6cbca 100644 --- a/css/biobank.css +++ b/css/biobank.css @@ -344,7 +344,7 @@ } .display .node.forbidden { - background: 2px solid #000000; + background: #DDDDDD; } .display .occupied:hover, .display .available:hover{ diff --git a/jsx/batchEditForm.js b/jsx/batchEditForm.js index 180188bb..1da3a2ee 100644 --- a/jsx/batchEditForm.js +++ b/jsx/batchEditForm.js @@ -309,12 +309,12 @@ class BatchEditForm extends React.PureComponent { ) : null; diff --git a/jsx/biobankIndex.js b/jsx/biobankIndex.js index a4a09a19..3a6905b3 100644 --- a/jsx/biobankIndex.js +++ b/jsx/biobankIndex.js @@ -233,12 +233,25 @@ class BiobankIndex extends React.Component { * @return {int} */ increaseCoordinate(coordinate, parentContainerId) { - const coordinates = this.state.data.containers[parentContainerId].coordinates; - + const containers = this.state.data.containers; + const childCoordinates = containers[parentContainerId].childContainerIds + .reduce((result, id) => { + const container = containers[id]; + if (container.coordinate) { + result[container.coordinate] = id; + } + return result; + }, {}); + const increment = (coord) => { - return coordinates.includes(coord) ? increment(coord + 1) : coord; + coord++; + if (childCoordinates.hasOwnProperty(coord)) { + coord = increment(coord); + } + + return coord; }; - + return increment(coordinate); } @@ -267,6 +280,7 @@ class BiobankIndex extends React.Component { const specimen = list[key]; specimen.candidateId = current.candidateId; specimen.sessionId = current.sessionId; + specimen.projectIds = projectIds; specimen.quantity = specimen.collection.quantity; specimen.unitId = specimen.collection.unitId; specimen.collection.centerId = centerId; @@ -279,7 +293,6 @@ class BiobankIndex extends React.Component { const container = specimen.container; container.statusId = availableId; container.temperature = 20; - container.projectIds = projectIds; container.centerId = centerId; container.originId = centerId; @@ -385,7 +398,6 @@ class BiobankIndex extends React.Component { Object.entries(list).forEach(([key, container]) => { container.statusId = availableId; container.temperature = 20; - container.projectIds = current.projectIds; container.originId = current.centerId; container.centerId = current.centerId; @@ -483,6 +495,7 @@ class BiobankIndex extends React.Component { 'unitId', 'candidateId', 'sessionId', + 'projectIds', 'collection', ]; const float = ['quantity']; @@ -682,7 +695,6 @@ class BiobankIndex extends React.Component { 'typeId', 'temperature', 'statusId', - 'projectIds', 'centerId', ]; diff --git a/jsx/container.js b/jsx/container.js index 99d39186..4bc3ea0a 100644 --- a/jsx/container.js +++ b/jsx/container.js @@ -79,11 +79,7 @@ class BiobankContainer extends Component { const coordinates = data.containers[container.id].childContainerIds .reduce((result, id) => { const container = data.containers[id]; - if (container === undefined) { - // if the container is undefined, user does not have permission - // to view it. - console.log('undefined'); - } else if (container.coordinate) { + if (container.coordinate) { result[container.coordinate] = id; } return result; @@ -130,10 +126,7 @@ class BiobankContainer extends Component { const child = data.containers[childId]; - if (child === undefined) { - // if the child container is undefined, user does not have permission - // to view it. - } else if (child.coordinate) { + if (child.coordinate) { listAssigned.push(
'+children[coord].barcode+'' + - '
'+optcon.types[children[coord].typeId].label+'
' + - '
'+optcon.stati[children[coord].statusId].label+'
'; - } - draggable = !loris.userHasPermission( - 'biobank_container_update') || - editable.loadContainer || - editable.containerCheckout - ? 'false' : 'true'; - onDragStart = drag; - - if (editable.containerCheckout) { - onClick = (e) => { - let container = data.containers[coordinates[e.target.id]]; - setCheckoutList(container); - }; - } - if (editable.loadContainer) { - onClick = null; - } + dataHtml = 'true'; + dataToggle = 'tooltip'; + dataPlacement = 'top'; + // This is to avoid a console error + if (children[coordinates[coordinate]]) { + const coord = coordinates[coordinate]; + tooltipTitle = + '
'+children[coord].barcode+'
' + + '
'+optcon.types[children[coord].typeId].label+'
' + + '
'+optcon.stati[children[coord].statusId].label+'
'; + } + draggable = !loris.userHasPermission( + 'biobank_container_update') || + editable.loadContainer || + editable.containerCheckout + ? 'false' : 'true'; + onDragStart = drag; + + if (editable.containerCheckout) { + onClick = (e) => { + let container = data.containers[coordinates[e.target.id]]; + setCheckoutList(container); + }; + } + if (editable.loadContainer) { + onClick = null; } onDragOver = null; onDrop = null; diff --git a/jsx/containerForm.js b/jsx/containerForm.js index 535020c5..fd22685a 100644 --- a/jsx/containerForm.js +++ b/jsx/containerForm.js @@ -84,17 +84,6 @@ class ContainerForm extends Component {
- { const container = data.containers[id]; - if (container == undefined) { - // if the container is undefined, the user doesn't have permission to - // to view it - } else if (container.coordinate) { + if (container.coordinate) { result[container.coordinate] = id; } return result; diff --git a/jsx/containerTab.js b/jsx/containerTab.js index 58e7e987..e52a44cf 100644 --- a/jsx/containerTab.js +++ b/jsx/containerTab.js @@ -57,8 +57,6 @@ class ContainerTab extends Component { return this.props.options.container.types[value].label; case 'Status': return this.props.options.container.stati[value].label; - case 'Projects': - return value.map((id) => this.props.options.projects[id]); case 'Site': return this.props.options.centers[value]; default: @@ -97,8 +95,6 @@ class ContainerTab extends Component { break; } return {value}; - case 'Projects': - return {value.join(', ')}; case 'Parent Barcode': return {value}; default: @@ -142,7 +138,6 @@ class ContainerTab extends Component { container.barcode, container.typeId, container.statusId, - container.projectIds, container.centerId, container.parentContainerId ? this.props.data.containers[container.parentContainerId].barcode : @@ -166,11 +161,6 @@ class ContainerTab extends Component { type: 'select', options: stati, }}, - {label: 'Projects', show: true, filter: { - name: 'project', - type: 'multiselect', - options: this.props.options.projects, - }}, {label: 'Site', show: true, filter: { name: 'currentSite', type: 'select', diff --git a/jsx/globals.js b/jsx/globals.js index 97e7bbc3..ae211926 100644 --- a/jsx/globals.js +++ b/jsx/globals.js @@ -212,16 +212,16 @@ function Globals(props) { ); - const projectField = ( + const projectField = () => specimen && ( props.edit('project')} - editValue={editContainer} - value={container.projectIds.length !== 0 ? - container.projectIds + editValue={props.editSpecimen} + value={specimen.projectIds.length !== 0 ? + specimen.projectIds .map((id) => options.projects[id]) .join(', ') : 'None'} editable={editable.project} @@ -229,11 +229,11 @@ function Globals(props) { ); @@ -267,7 +267,7 @@ function Globals(props) { const parentSpecimenField = () => { if (!specimen) { - return null; // Return null if specimen is undefined or null to handle edge case + return null; } const { parentSpecimenIds, parentSpecimenBarcodes } = specimen; @@ -386,7 +386,7 @@ function Globals(props) { {fTCycleField()} {temperatureField} {statusField} - {projectField} + {projectField()} {drawField} {centerField} {shipmentField()} diff --git a/jsx/specimenForm.js b/jsx/specimenForm.js index 5b970ddb..61418c0a 100644 --- a/jsx/specimenForm.js +++ b/jsx/specimenForm.js @@ -345,7 +345,7 @@ class SpecimenForm extends React.Component { required={true} value={current.projectIds} disabled={current.candidateId ? false : true} - errorMessage={errors.container.projectIds} + errorMessage={errors.specimen.projectIds} /> {renderRemainingQuantityFields()}
diff --git a/jsx/specimenTab.js b/jsx/specimenTab.js index a4a3ec26..2659dc1d 100644 --- a/jsx/specimenTab.js +++ b/jsx/specimenTab.js @@ -202,7 +202,7 @@ class SpecimenTab extends Component { options.sessions[specimen.sessionId].label, specimen.poolId ? (data.pools[specimen.poolId]||{}).label : null, container.statusId, - container.projectIds, + specimen.projectIds, container.centerId, options.sessionCenters[specimen.sessionId].centerId, specimen.collection.date, diff --git a/php/container.class.inc b/php/container.class.inc index f7e7f0d2..b3b5b60c 100644 --- a/php/container.class.inc +++ b/php/container.class.inc @@ -47,13 +47,11 @@ class Container implements * @var int $dimensionId * @var float $temperature * @var int $statusId - * @var array $projectIds - * @var array $shipmentBarocdes + * @var array $shipmentBarcodes * @var \CenterID $centerId * @var int $parentContainerId - * @var string $parentContainerBarocde + * @var ?string $parentContainerBarcode * @var array $childContainerIds - * @var array $coordinates * @var int $coordinate * @var string $lotNumber * @var \DateTime $expirationDate @@ -68,13 +66,11 @@ class Container implements private $dimensionId; private $temperature; private $statusId; - private $projectIds; private $shipmentBarcodes; private \CenterID $centerId; private $parentContainerId; - private string $parentContainerBarocde; + private ?string $parentContainerBarcode = null; private $childContainerIds; - private array $coordinates; private $coordinate; private $lotNumber; private $expirationDate; @@ -227,28 +223,6 @@ class Container implements return $this->temperature; } - /** - * Sets the ID of the container's current projects - * - * @param int[] $projectIds the IDs of the container's current projects - * - * @return void - */ - public function setProjectIds(array $projectIds) : void - { - $this->projectIds = $projectIds; - } - - /** - * Gets the ID of the container's current projects - * - * @return ?array - */ - public function getProjectIds() : ?array - { - return $this->projectIds; - } - /** * Sets the Barcodes of the container's shipments * @@ -262,7 +236,7 @@ class Container implements } /** - * Gets the Barocdes of the container's shipments + * Gets the Barcodes of the container's shipments * * @return ?array */ @@ -383,28 +357,6 @@ class Container implements return $this->childContainerIds; } - /** - * Sets the coordinates of the children containers - * - * @param array $coordinates - * - * @return void - */ - public function setCoordinates(array $coordinates) : void - { - $this->coordinates = $coordinates; - } - - /** - * Gets the coordinates of the children containers - * - * @return ?array - */ - public function getCoordinates() : ?array - { - return $this->coordinates; - } - /** * Sets the container's current coordinate in storage * @@ -525,9 +477,6 @@ class Container implements if (isset($data['statusId'])) { $this->setStatusId((int) $data['statusId']); } - if (isset($data['projectIds'])) { - $this->setProjectIds($data['projectIds']); - } if (isset($data['shipmentBarcodes'])) { $this->setShipmentBarcodes($data['shipmentBarcodes']); } @@ -543,9 +492,6 @@ class Container implements if (isset($data['childContainerIds'])) { $this->setChildContainerIds($data['childContainerIds']); } - if (isset($data['coordinates'])) { - $this->setCoordinates($data['coordinates']); - } if (isset($data['coordinate'])) { $this->setCoordinate((int) $data['coordinate']); } @@ -593,13 +539,11 @@ class Container implements 'dimensionId' => $this->dimensionId, 'temperature' => $this->temperature, 'statusId' => $this->statusId, - 'projectIds' => $this->projectIds, 'shipmentBarcodes' => $this->shipmentBarcodes, 'centerId' => $this->centerId, 'parentContainerId' => $this->parentContainerId, - 'parentContainerBarcode' => $this->getParentContainerBarcode, + 'parentContainerBarcode' => $this->parentContainerBarcode, 'childContainerIds' => $this->childContainerIds, - 'coordinates' => $this->coordinates, 'coordinate' => $this->coordinate, 'lotNumber' => $this->lotNumber, 'expirationDate' => $expirationDate, diff --git a/php/containercontroller.class.inc b/php/containercontroller.class.inc index 037c6e6c..e63edcf1 100644 --- a/php/containercontroller.class.inc +++ b/php/containercontroller.class.inc @@ -76,7 +76,7 @@ class ContainerController $this->_validatePermission('view'); $containers = []; $this->dao = $this->dao - ->filter(new \LORIS\Data\Filters\UserProjectOrSiteMatch()); + ->filter(new \LORIS\Data\Filters\UserSiteMatch()); $containerIt = $this->dao->execute($this->user); foreach ($containerIt as $id => $container) { $containers[$id] = $container; @@ -162,17 +162,7 @@ class ContainerController */ private function _getDataProvisioner() : ContainerDAO { - $dao = new ContainerDAO($this->db); - // if ($this->user->hasPermission('access_all_profiles') === false) { - // $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - // } - // if ($this->user->hasPermission('biobank_container_view') === false) { - // $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 0)); - // } - // if ($this->user->hasPermission('biobank_specimen_view') === false) { - // $dao = $dao->filter(new PrimaryContainerFilter($this->loris, 1)); - // } - return $dao; + return new ContainerDAO($this->db); } /** diff --git a/php/containerdao.class.inc b/php/containerdao.class.inc index a8937054..8e144ecd 100644 --- a/php/containerdao.class.inc +++ b/php/containerdao.class.inc @@ -79,52 +79,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance )[$id] ?? null; } - // /** - // * Returns an array of all the Child Container Barcodes associated with - // * the given Container ID from the biobank_container_parent table. - // * - // * @param ?int $id - // * - // * @return array $childContainerBarocdes - // */ - // private function _getChildContainerBarcodes(?int $id) : array - // { - // $query = 'SELECT bcp.Coordinate, - // bc2.Barcode - // FROM biobank_container bc - // LEFT JOIN biobank_container_parent bc - // ON bc.ContainerID=bcp.ParentContainerID - // LEFT JOIN biobank_container bc2 - // ON bcp.ContainerID=bc2.ContainerID - // WHERE bc.ContainerID=:i'; - // $childContainerBarcodes = $this->db->pselectColWithIndexKey( - // $query, - // ['i' => $id], - // 'Coordinate' - // ); - - // return $childContainerBarcodes; - // } - - /** - * Returns an array of all the Project IDs associated with the given - * Container ID from the biobank_container_project_rel table. - * - * @param ?int $id of Container - * - * @return $projectIds List of Project Ids that are associated with the - * Container ID. - */ - private function _getProjectIds(?int $id) : array - { - $query = 'SELECT ProjectID - FROM biobank_container_project_rel - WHERE ContainerID=:i'; - $projectIds = $this->db->pselectCol($query, ['i' => $id]); - - return $projectIds; - } - // XXX: Currently this function is never used with any conditions passed as // parameters. Decide if this is a necessary feature. It is likely useful, // but I need to figure out how to integrate it wit the dataprovisioner. @@ -149,14 +103,12 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance bct.ContainerCapacityID, bct.ContainerDimensionID, bc.ContainerStatusID, - GROUP_CONCAT(DISTINCT(bcpr.ProjectID)) as ProjectIDs, GROUP_CONCAT(DISTINCT(s.Barcode)) as ShipmentBarcodes, bc.Temperature, bc.CenterID, bcp.ParentContainerID, bc2.Barcode as ParentContainerBarcode, GROUP_CONCAT(DISTINCT bcp2.ContainerID ORDER BY bcp2.ContainerID) as ChildContainerIDs, - GROUP_CONCAT(DISTINCT bcp2.Coordinate ORDER BY bcp2.ContainerID) as Coordinates, bcp.Coordinate as Coordinate, bc.LotNumber, bc.ExpirationDate, @@ -172,8 +124,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance ON bc.ContainerID=bcp2.ParentContainerID LEFT JOIN biobank_container_type bct ON bc.ContainerTypeID=bct.ContainerTypeID - LEFT JOIN biobank_container_project_rel bcpr - ON bc.ContainerID=bcpr.ContainerID LEFT JOIN biobank_container_shipment_rel bcsr ON bc.ContainerID=bcsr.ContainerID LEFT JOIN shipment s @@ -198,8 +148,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $containers[$id] = $this->_getInstanceFromSQL( $containerRow, explode(',', $containerRow['ChildContainerIDs'] ?? ''), - explode(',', $containerRow['Coordinates'] ?? ''), - array_map(fn($id) => new \ProjectID($id), explode(',', $containerRow['ProjectIDs'])), explode(',', $containerRow['ShipmentBarcodes'] ?? '') ); } @@ -325,6 +273,8 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance return $stati; } + + // TODO: Not being used. Delete if not useful by November 2023. /** * Queries all rows of the biobank_container_parent table and returns a * nested array with the Parent Container ID field as the first index, the @@ -338,37 +288,37 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance * indexed by Parent Container ID and * Coordinate value or Unassigned. */ - public function getCoordinates() : array - { - $query = "SELECT ContainerID as childContainerId, - ParentContainerID as parentContainerId, - Coordinate as coordinate - Barcode as barcode - FROM biobank_container_parent - LEFT JOIN biobank_container - USING (ContainerID)"; - $result = $this->db->pselect($query, []); - $coordinates = []; - foreach ($result as $row) { - if (!isset($coordinates[$row['parentContainerId']])) { - $coordinates[$row['parentContainerId']] = []; - }; - $container =& $coordinates[$row['parentContainerId']]; - if (empty($row['coordinate'])) { - $container['Unassigned'][] = [ - 'id' => $row['childContainerId'], - 'barcode' => $row['barcode'], - ]; - } else { - $container[$row['coordinate']] = [ - 'id' => $row['childContainerId'], - 'barcode' => $row['barcode'], - ]; - } - } - - return $coordinates; - } + // public function getCoordinates() : array + // { + // $query = "SELECT ContainerID as childContainerId, + // ParentContainerID as parentContainerId, + // Coordinate as coordinate + // Barcode as barcode + // FROM biobank_container_parent + // LEFT JOIN biobank_container + // USING (ContainerID)"; + // $result = $this->db->pselect($query, []); + // $coordinates = []; + // foreach ($result as $row) { + // if (!isset($coordinates[$row['parentContainerId']])) { + // $coordinates[$row['parentContainerId']] = []; + // }; + // $container =& $coordinates[$row['parentContainerId']]; + // if (empty($row['coordinate'])) { + // $container['Unassigned'][] = [ + // 'id' => $row['childContainerId'], + // 'barcode' => $row['barcode'], + // ]; + // } else { + // $container[$row['coordinate']] = [ + // 'id' => $row['childContainerId'], + // 'barcode' => $row['barcode'], + // ]; + // } + // } + + // return $coordinates; + // } /** * This function receives a Container Object, converts it into a SQL format @@ -402,12 +352,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance // set the respective data array index to the value of the new // container ID. $data['biobank_container_parent']['ContainerID'] = $container->getId(); - foreach ($data['biobank_container_project_rel'] as $id => $insert) { - $projdata =& $data['biobank_container_project_rel'][$id]; - $projdata['ContainerID'] = $container->getId(); - } $this->saveParentContainer($container, $data); - $this->_saveProject($container, $data); return $this->getInstanceFromId($container->getId()); } @@ -439,7 +384,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $updatedContainers = [$container]; $this->saveParentContainer($container, $data); - $this->_saveProject($container, $data); //Cascade changes in temperature, status, and center to all child Containers if ($oldContainer->getParentContainerId()) { @@ -471,11 +415,11 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance ); $updatedContainers = array_merge($updatedChildren, $updatedContainers); } - if ($container->getCenterId()->__toString() !== $oldContainer->getCenterId()->__toString()) { + if ($container->getCenterId() != $oldContainer->getCenterId()) { $updatedChildren = $this->_cascadeToChildren( $container, 'CenterID', - $container->getCenterId()->__toString() + $container->getCenterId() ); $updatedContainers = array_merge($updatedChildren, $updatedContainers); } @@ -513,29 +457,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance } } - /** - * Saves the container to the LORIS SQL database. - * - * @param Container $container The container to save - * @param array $data The data to save - * - * @return void - */ - private function _saveProject($container, $data) - { - // insert on update biobank_container_project_rel with relevant data. - $this->db->delete( - 'biobank_container_project_rel', - ['ContainerID' => $container->getId()] - ); - foreach ($data['biobank_container_project_rel'] as $insert) { - $this->db->insert( - 'biobank_container_project_rel', - $insert - ); - } - } - /** * This recursive function cascades the $value to the specified $field of * all the children of the $container Object. @@ -623,7 +544,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance 'ContainerTypeID' => $container->getTypeId(), 'Temperature' => $container->getTemperature(), 'ContainerStatusID' => $container->getStatusId(), - 'CenterID' => $container->getCenterId()->__toString(), + 'CenterID' => $container->getCenterId(), 'LotNumber' => $container->getLotNumber(), 'ExpirationDate' => $expirationDate, 'Comments' => $container->getComments(), @@ -635,19 +556,9 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance 'Coordinate' => $container->getCoordinate(), ]; - $containerProjectData = []; - - foreach ($container->getProjectIds() as $id) { - $containerProjectData[$id] = [ - 'ContainerID' => $container->getId(), - 'ProjectID' => $id, - ]; - } - return [ 'biobank_container' => $containerData, 'biobank_container_parent' => $parentData, - 'biobank_container_project_rel' => $containerProjectData, ]; } @@ -657,7 +568,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance * * @param array $data Values to be instantiated. * @param ?array $childContainerIds List of child container ids. - * @param ?array $projectIds List of project ids. * @param ?array $shipmentBarcodes List of barcodes. * * @return Container @@ -665,8 +575,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance private function _getInstanceFromSQL( array $data, ?array $childContainerIds, - ?array $coordinates, - ?array $projectIds, ?array $shipmentBarcodes ) : Container { $container = $this->getInstance(); @@ -692,9 +600,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['ContainerStatusID'])) { $container->setStatusId((int) $data['ContainerStatusID']); } - if (isset($projectIds)) { - $container->setProjectIds($projectIds); - } if (isset($shipmentBarcodes)) { $container->setShipmentBarcodes($shipmentBarcodes); } @@ -710,9 +615,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance if (isset($childContainerIds)) { $container->setChildContainerIds($childContainerIds); } - if (isset($coordinates)) { - $container->setCoordinates($coordinates); - } if (isset($data['Coordinate'])) { $container->setCoordinate((int) $data['Coordinate']); } diff --git a/php/optionsendpoint.class.inc b/php/optionsendpoint.class.inc index 70e7acc1..d985497b 100644 --- a/php/optionsendpoint.class.inc +++ b/php/optionsendpoint.class.inc @@ -145,15 +145,11 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface LEFT JOIN session s USING (CandID) LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) LEFT JOIN biobank_container bc USING (ContainerID) - LEFT JOIN candidate_diagnosis_rel USING (CandID)"; - $where = ' '; - // XXX: This is commented out because the access all profiles permission - // is causing some issues. - // if (!$user->hasPermission('access_all_profiles')) { - $where = " WHERE s.CenterID IN ($userCenters) OR s.ProjectID IN ($userProjects)"; - // } - $group = " GROUP BY CandID"; - $candidates = $db->pselectWithIndexKey($query.$where.$group, array(), 'id'); + LEFT JOIN candidate_diagnosis_rel USING (CandID) + WHERE s.CenterID IN ($userCenters) OR s.ProjectID IN ($userProjects) + GROUP BY + CandID"; + $candidates = $db->pselectWithIndexKey($query, array(), 'id'); foreach ($candidates as $id => $candidate) { $candidate['diagnosisIds'] = $candidate['diagnosisIds'] ? explode(',', $candidate['diagnosisIds']) @@ -198,20 +194,20 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface // XXX: This should eventually be replaced by a call directly to a // Session Controller or Session Options endpoint that will be able to // provide these options. - $query = "SELECT c.CandID as candidateId, - s.ID sessionId, - s.Visit_label as label, - s.CenterID as centerId - FROM candidate c - LEFT JOIN session s USING(CandID) - LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) - LEFT JOIN biobank_container bc USING (ContainerID)"; - $where = ' '; - if (!$user->hasPermission('access_all_profiles')) { - $where = " WHERE s.CenterID IN ($userCenters) - OR bc.CenterID IN ($userCenters)"; - } - $result = $db->pselect($query.$where, array()); + $query = "SELECT + c.CandID as candidateId, + s.ID sessionId, + s.Visit_label as label, + s.CenterID as centerId + FROM + candidate c + LEFT JOIN session s USING (CandID) + LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) + LEFT JOIN biobank_container bc USING (ContainerID) + WHERE + s.CenterID IN ($userCenters) OR + bc.CenterID IN ($userCenters)"; + $result = $db->pselect($query, array()); $candidateSessions = []; $sessionCenters = []; foreach ($result as $row) { diff --git a/php/pool.class.inc b/php/pool.class.inc index bafa3154..5b9a554c 100644 --- a/php/pool.class.inc +++ b/php/pool.class.inc @@ -64,8 +64,8 @@ class Pool implements private string $candidatePSCID; private $sessionId; private $typeId; - private $centerId; - private $projectIds; + private \CenterID $centerId; + private array $projectIds; private $date; private $time; @@ -438,9 +438,6 @@ class Pool implements if (isset($data['projectIds'])) { $this->setProjectIds($data['projectIds']); } - if (isset($data['projectIds'])) { - $this->setProjectIds($data['projectIds']); - } if (isset($data['date'])) { $this->setDate(new \DateTime($data['date'])); } diff --git a/php/poolcontroller.class.inc b/php/poolcontroller.class.inc index a2d93714..9a842096 100644 --- a/php/poolcontroller.class.inc +++ b/php/poolcontroller.class.inc @@ -215,7 +215,7 @@ class PoolController throw new BadRequest('Pooled specimens must be of the same Type'); } - if ($baseContainer->getCenterId()->__toString() !== $container->getCenterId()->__toString()) { + if ($baseContainer->getCenterId() != $container->getCenterId()) { throw new BadRequest('Pooled specimens must be at the same Site'); } diff --git a/php/pooldao.class.inc b/php/pooldao.class.inc index 721b0ae5..b849eaa0 100644 --- a/php/pooldao.class.inc +++ b/php/pooldao.class.inc @@ -287,8 +287,6 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['TypeID'])) { $pool->setTypeId((int) $data['TypeID']); } - if (isset($projectIds)) { - $pool->setProjectIds($projectIds); if (isset($data['CenterID'])) { $pool->setCenterId($data['CenterID']); } diff --git a/php/specimen.class.inc b/php/specimen.class.inc index 74aa7f1d..36147b18 100644 --- a/php/specimen.class.inc +++ b/php/specimen.class.inc @@ -66,7 +66,7 @@ class Specimen implements private $quantity; private $unitId; private $fTCycle; - private $centerId; + private \CenterID $centerId; private $projectIds; private $parentSpecimenIds; private $parentSpecimenBarcodes; @@ -531,10 +531,8 @@ class Specimen implements if (isset($data['unitId'])) { $this->setUnitId((int) $data['unitId']); } - if (isset($data['containerCenterId'])) { - $this->setContainerCenterId(new \CenterID(strval($data['containerCenterId']))); - if (isset($data['projectIds'])) { - $this->setProjectIds($data['projectIds']); + if (isset($data['centerId'])) { + $this->setCenterId(new \CenterID(strval($data['centerId']))); } if (isset($data['projectIds'])) { $this->setProjectIds($data['projectIds']); diff --git a/php/specimendao.class.inc b/php/specimendao.class.inc index 9c48fc8d..407fc1da 100644 --- a/php/specimendao.class.inc +++ b/php/specimendao.class.inc @@ -62,7 +62,7 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance */ public function getInstanceFromId(int $id) : Specimen { - return $this->selectInstances([['column'=>'SpecimenID', 'value'=>$id]])[$id]; + return $this->selectInstances([['column'=>'bs.SpecimenID', 'value'=>$id]])[$id]; } /** @@ -83,6 +83,24 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance return $this->db->pselectcol($query, ['i' => $id]); } + /** + * Returns an array of all the Project IDs associated with the given + * Specimen from the biobank_specimen_project_rel table. + * + * @param ?int $id + * + * @return $projectIds + */ + private function _getProjectIds(?int $id) : array + { + $query = 'SELECT ProjectID + FROM biobank_specimen_project_rel + WHERE SpecimenID=:i'; + $projectIds = $this->db->pselectCol($query, ['i' => $id]); + + return $projectIds; + } + /** * This will select all specimens from the database that match the * attribute values passed by $conditions and will return an array @@ -97,23 +115,25 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance ?array $conditions = [], string $operator = 'AND' ) : array { - $query = "SELECT bs.SpecimenID, + $query = "SELECT + bs.SpecimenID, bs.ContainerID, bc.Barcode, bs.SpecimenTypeID, bs.Quantity, bs.UnitID, bc.CenterID, - GROUP_CONCAT(DISTINCT(bcpr.ProjectID)) as ProjectIDs, + GROUP_CONCAT(DISTINCT(bsprr.ProjectID)) as ProjectIDs, bsf.FreezeThawCycle, - GROUP_CONCAT(bspa.ParentSpecimenID) as ParentSpecimenIDs, + GROUP_CONCAT(DISTINCT bspa.ParentSpecimenID) as ParentSpecimenIDs, + GROUP_CONCAT(DISTINCT bc2.Barcode) as ParentSpecimenBarcodes, s.CandID as CandidateID, c.PSCID as CandidatePSCID, bs.SessionID, ( SELECT FLOOR(DATEDIFF(s.Date_visit, c.DoB)/365.25) ) as CandidateAge, - bspr.PoolID, + bspor.PoolID, bsc.SpecimenProtocolID as CollectionProtocolID, bsc.Quantity as CollectionQuantity, bsc.UnitID as CollectionUnitID, @@ -138,25 +158,30 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance bsa.Comments as AnalysisComments, bsa.Data as AnalysisData FROM biobank_specimen bs - JOIN biobank_container bc USING (ContainerID) - LEFT JOIN biobank_container_project_rel bcpr - ON bc.ContainerID=bcpr.ContainerID + INNER JOIN biobank_container bc + ON bs.ContainerID=bc.ContainerID + LEFT JOIN biobank_specimen_project_rel bsprr + ON bs.SpecimenID=bsprr.SpecimenID LEFT JOIN biobank_specimen_freezethaw bsf - USING (SpecimenID) + ON bs.SpecimenID=bsf.SpecimenID LEFT JOIN session s ON bs.SessionID=s.ID LEFT JOIN candidate c ON s.CandID=c.CandID - LEFT JOIN biobank_specimen_pool_rel bspr - USING (SpecimenID) + LEFT JOIN biobank_specimen_pool_rel bspor + ON bs.SpecimenID=bspor.SpecimenID LEFT JOIN biobank_specimen_collection bsc - USING (SpecimenID) + ON bs.SpecimenID=bsc.SpecimenID LEFT JOIN biobank_specimen_preparation bsp - USING (SpecimenID) + ON bs.SpecimenID=bsp.SpecimenID LEFT JOIN biobank_specimen_analysis bsa - USING (SpecimenID) + ON bs.SpecimenID=bsa.SpecimenID LEFT JOIN biobank_specimen_parent bspa - USING (SpecimenID)"; + ON bs.SpecimenID=bspa.SpecimenID + LEFT JOIN biobank_specimen bs2 + ON bspa.ParentSpecimenID=bs2.SpecimenID + LEFT JOIN biobank_container bc2 + ON bs2.ContainerID=bc2.ContainerID"; if (!empty($conditions)) { $whereClause = []; foreach ($conditions as $condition) { @@ -170,38 +195,21 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance } $query .= " GROUP BY bs.SpecimenID"; $specimenRows = $this->db->pselectWithIndexKey($query, [], 'SpecimenID'); - $specimens = []; - if (!empty($specimenRows)) { - foreach ($specimenRows as $id => $specimenRow) { - $parentSpecimenIds = $specimenRow['ParentSpecimenIDs'] - ? explode(',', $specimenRow['ParentSpecimenIDs']) - : []; - $parentSpecimenBarcodes = []; - foreach ($parentSpecimenIds as $parentSpecimenId) { - $parentSpecimen = $this->getInstanceFromId((int) $parentSpecimenId); - $parentsSpecimenBarcodes[] = $parentSpecimen->getBarcode(); - } - - $projectIds = array_map(function($projectId) { - return new \ProjectID($projectId); // store projectId within object - }, explode(',', $specimenRow['ProjectIDs'])); - - $projectIds = array_map(function($projectId) { - return new \ProjectID($projectId); // store projectId within object - }, explode(',', $specimenRow['ProjectIDs'])); - - $specimen = $this->_getInstanceFromSQL( - $specimenRow, - $projectIds, - $parentSpecimenIds, - $parentSpecimenBarcodes, - ); - - $specimens[$id] = $specimen; - } + foreach ($specimenRows as $id => $specimenRow) { + $parentSpecimenIds = explode(',', $specimenRow['ParentSpecimenIDs'] ?? ''); + $parentSpecimenBarcodes = explode(',', $specimenRow['parentSpecimenBarcodes'] ?? ''); + $projectIds = array_map(fn($id) => new \ProjectID($id), + explode(',', $specimenRow['ProjectIDs'])); + + $specimens[$id] = $this->_getInstanceFromSQL( + $specimenRow, + $projectIds, + $parentSpecimenIds, + $parentSpecimenBarcodes + ); } - + return $specimens; } @@ -544,9 +552,39 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance ); } + // Prepare project data and sent it to be saved. + foreach ($data['biobank_specimen_project_rel'] as $id => $insert) { + $projdata =& $data['biobank_container_project_rel'][$id]; + $projdata['SpecimenID'] = $specimen->getId(); + } + $this->_saveProject($specimen, $data); + return $this->getInstanceFromId($specimen->getId()); } + /** + * Saves the Specimen Project to the database. + * + * @param Specimen $specimen + * @param array $data The data to save + * + * @return void + */ + private function _saveProject(Specimen $specimen, array $data) + { + // insert on update biobank_specimen_project_rel with relevant data. + $this->db->delete( + 'biobank_specimen_project_rel', + ['SpecimenID' => $specimen->getId()] + ); + foreach ($data['biobank_specimen_project_rel'] as $insert) { + $this->db->insert( + 'biobank_specimen_project_rel', + $insert + ); + } + } + /** * This function takes a Specimen Instance and prepares the data to be * inserted into the database by converting it to a data array. This one to @@ -625,12 +663,21 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance json_encode($analysis->getData()) : null; } + $containerProjectData = []; + foreach ($container->getProjectIds() as $id) { + $containerProjectData[$id] = [ + 'ContainerID' => $container->getId(), + 'ProjectID' => $id, + ]; + } + return [ - 'biobank_specimen' => $specimenData, - 'biobank_specimen_freezethaw' => $freezeThawData, - 'biobank_specimen_collection' => $collectionData, - 'biobank_specimen_preparation' => $preparationData, - 'biobank_specimen_analysis' => $analysisData, + 'biobank_specimen' => $specimenData, + 'biobank_specimen_freezethaw' => $freezeThawData, + 'biobank_specimen_collection' => $collectionData, + 'biobank_specimen_preparation' => $preparationData, + 'biobank_specimen_analysis' => $analysisData, + 'biobank_container_project_rel' => $containerProjectData, ]; } @@ -677,15 +724,16 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['FreezeThawCycle'])) { $specimen->setFTCycle((int) $data['FreezeThawCycle']); } - if (!empty($parentSpecimenIds)) { $specimen->setParentSpecimenIds($parentSpecimenIds); - $specimen->setParentSpecimenBarcodes($parentSpecimenBarcodes); } else { $specimen->setParentSpecimenIds([]); + } + if (!empty($parentSpecimenBarcodes)) { + $specimen->setParentSpecimenBarcodes($parentSpecimenBarcodes); + } else { $specimen->setParentSpecimenBarcodes([]); } - if (isset($data['CandidateID'])) { $specimen->setCandidateId((int) $data['CandidateID']); } diff --git a/sql/2023-08-10_DropCenterfromPoolandTransferProjectsfromContainerstoSpecimens.sql b/sql/2023-08-10_DropCenterfromPoolandTransferProjectsfromContainerstoSpecimens.sql new file mode 100644 index 00000000..802da700 --- /dev/null +++ b/sql/2023-08-10_DropCenterfromPoolandTransferProjectsfromContainerstoSpecimens.sql @@ -0,0 +1,28 @@ + +-- DROP Center From Pools +ALTER TABLE biobank_pool +DROP FOREIGN KEY FK_biobank_pool_CenterID, +DROP COLUMN CenterID; + +-- Add new specimen project relational table +CREATE TABLE `biobank_specimen_project_rel` ( + `SpecimenID` integer unsigned NOT NULL, + `ProjectID` int(10) unsigned NOT NULL, + CONSTRAINT `PK_biobank_specimen_project_rel` PRIMARY KEY (`SpecimenID`, `ProjectID`), + CONSTRAINT `FK_biobank_specimen_project_rel_SpecimenID` + FOREIGN KEY (`SpecimenID`) REFERENCES `biobank_specimen`(`SpecimenID`) + ON UPDATE RESTRICT ON DELETE RESTRICT, + CONSTRAINT `FK_biobank_specimen_project_rel_ProjectID` + FOREIGN KEY (`ProjectID`) REFERENCES `Project`(`ProjectID`) + ON UPDATE RESTRICT ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Insert projects and specimens +INSERT INTO biobank_specimen_project_rel (SpecimenID, ProjectID) +SELECT bsp.SpecimenID, rcp.ProjectID +FROM biobank_container_project_rel rcp +LEFT JOIN biobank_container bc ON rcp.ContainerID = bc.ContainerID +RIGHT JOIN biobank_specimen bsp ON bc.ContainerID = bsp.ContainerID; + +-- Drop old table +DROP TABLE biobank_container_project_rel; From 76052142ce1e765f148fd9b6089a0370f567fdfe Mon Sep 17 00:00:00 2001 From: Rida Abou-Haidar Date: Mon, 21 Aug 2023 10:15:17 -0400 Subject: [PATCH 03/19] added corrections from testing --- jsx/barcodePage.js | 2 -- jsx/globals.js | 12 +++---- jsx/specimenTab.js | 8 ++--- php/container.class.inc | 58 ++++++++++++++++++++++--------- php/containercontroller.class.inc | 8 +++-- php/containerdao.class.inc | 30 ++++++++++------ php/optionsendpoint.class.inc | 18 +++++----- php/pooldao.class.inc | 17 ++++----- php/specimendao.class.inc | 47 +++++++++---------------- 9 files changed, 112 insertions(+), 88 deletions(-) diff --git a/jsx/barcodePage.js b/jsx/barcodePage.js index db49b527..a5fd433e 100644 --- a/jsx/barcodePage.js +++ b/jsx/barcodePage.js @@ -217,10 +217,8 @@ class BarcodePage extends Component { * @param {string} value - the error message */ setErrors(name, value) { - console.log(name+': '+value); const errors = clone(this.state.errors); errors[name] = value; - console.log(errors); this.setState({errors}); } diff --git a/jsx/globals.js b/jsx/globals.js index ae211926..1be3388d 100644 --- a/jsx/globals.js +++ b/jsx/globals.js @@ -217,9 +217,9 @@ function Globals(props) { loading={props.loading} label='Projects' clearAll={props.clearAll} - updateValue={props.updateSpecimen} + updateValue={()=>props.updateSpecimen(current.specimen)} edit={() => props.edit('project')} - editValue={props.editSpecimen} + editValue={() => props.editSpecimen(specimen)} value={specimen.projectIds.length !== 0 ? specimen.projectIds .map((id) => options.projects[id]) @@ -271,12 +271,12 @@ function Globals(props) { } const { parentSpecimenIds, parentSpecimenBarcodes } = specimen; - const value = parentSpecimenIds?.length === 0 + const value = parentSpecimenIds.length === 0 ? 'None' - : Object.values(parentSpecimenBarcodes) + : parentSpecimenBarcodes .map(barcode => {barcode}) - .join(', '); - + .reduce((prev, curr, index) => [prev, index == 0 ? '' : ', ', curr]); + return ( statusId; } + /** + * Sets Project IDs of affiliated container specimen. + * + * @param \ProjectID[] + */ + public function setProjectIds(array $projectIds) + { + $this->projectIds = $projectIds; + } + + /** + * Return Project ID of affiliated specimen container. + * + * @return \ProjectID[] + */ + public function getProjectIds() : array + { + return $this->projectIds; + } + /** * Sets the ID of the container's current centerId * @@ -480,6 +502,9 @@ class Container implements if (isset($data['shipmentBarcodes'])) { $this->setShipmentBarcodes($data['shipmentBarcodes']); } + if (isset($data['projectIds'])) { + $this->setProjectIds($data['projectIds']); + } if (isset($data['centerId'])) { $this->setCenterId(new \CenterID(strval($data['centerId']))); } @@ -540,6 +565,7 @@ class Container implements 'temperature' => $this->temperature, 'statusId' => $this->statusId, 'shipmentBarcodes' => $this->shipmentBarcodes, + 'projectIds' => $this->projectIds, 'centerId' => $this->centerId, 'parentContainerId' => $this->parentContainerId, 'parentContainerBarcode' => $this->parentContainerBarcode, diff --git a/php/containercontroller.class.inc b/php/containercontroller.class.inc index e63edcf1..e69ace8f 100644 --- a/php/containercontroller.class.inc +++ b/php/containercontroller.class.inc @@ -76,7 +76,7 @@ class ContainerController $this->_validatePermission('view'); $containers = []; $this->dao = $this->dao - ->filter(new \LORIS\Data\Filters\UserSiteMatch()); + ->filter(new \LORIS\Data\Filters\UserProjectOrSiteMatch()); $containerIt = $this->dao->execute($this->user); foreach ($containerIt as $id => $container) { $containers[$id] = $container; @@ -206,7 +206,6 @@ class ContainerController $this->_validateTypeId($container); $this->_validateTemperature($container); $this->_validateStatusId($container); - $this->_validateProjectIds($container); $this->_validateShipmentBarcodes($container); $this->_validateCenterId($container); $this->_validateParentContainerId($container); @@ -385,8 +384,13 @@ class ContainerController */ private function _validateShipmentBarcodes(Container $container) : void { + if (empty($container->getShipmentBarcodes())) { + // Handle the null case here + return; + } $shipmentDAO = new ShipmentDAO($this->db); foreach ($container->getShipmentBarcodes() as $barcode) { + error_log($barcode); $shipment = $shipmentDAO->getInstanceFromBarcode($barcode); if ($shipment->getActive()) { throw new BadRequest( diff --git a/php/containerdao.class.inc b/php/containerdao.class.inc index 8e144ecd..d3d77b22 100644 --- a/php/containerdao.class.inc +++ b/php/containerdao.class.inc @@ -105,6 +105,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance bc.ContainerStatusID, GROUP_CONCAT(DISTINCT(s.Barcode)) as ShipmentBarcodes, bc.Temperature, + GROUP_CONCAT(DISTINCT(bspr.ProjectID)) as ProjectIDs, bc.CenterID, bcp.ParentContainerID, bc2.Barcode as ParentContainerBarcode, @@ -129,7 +130,9 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance LEFT JOIN shipment s ON bcsr.ShipmentID=s.ShipmentID LEFT JOIN biobank_specimen bs - ON bc.ContainerID=bs.ContainerID'; + ON bc.ContainerID=bs.ContainerID + LEFT JOIN biobank_specimen_project_rel bspr + ON bs.SpecimenID=bspr.SpecimenID'; if (!empty($conditions)) { $whereClause = []; foreach ($conditions as $condition) { @@ -145,11 +148,7 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance $containers = []; foreach ($containerRows as $id => $containerRow) { - $containers[$id] = $this->_getInstanceFromSQL( - $containerRow, - explode(',', $containerRow['ChildContainerIDs'] ?? ''), - explode(',', $containerRow['ShipmentBarcodes'] ?? '') - ); + $containers[$id] = $this->_getInstanceFromSQL($containerRow); } return $containers; } @@ -574,8 +573,6 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance */ private function _getInstanceFromSQL( array $data, - ?array $childContainerIds, - ?array $shipmentBarcodes ) : Container { $container = $this->getInstance(); @@ -600,8 +597,18 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['ContainerStatusID'])) { $container->setStatusId((int) $data['ContainerStatusID']); } - if (isset($shipmentBarcodes)) { + if (isset($data['ShipmentBarcodes'])) { + $shipmentBarcodes = explode(',', $data['ShipmentBarcodes']); $container->setShipmentBarcodes($shipmentBarcodes); + } else { + $container->setShipmentBarcodes([]); + } + if (isset($data['ProjectIDs'])) { + $projectIds = array_map(fn($id) => new \ProjectID($id), + explode(',', $data['ProjectIDs'])); + $container->setProjectIds($projectIds); + } else { + $container->setProjectIds([]); } if (isset($data['CenterID'])) { $container->setCenterId(new \CenterID(strval($data['CenterID']))); @@ -612,8 +619,11 @@ class ContainerDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['ParentContainerBarcode'])) { $container->setParentContainerBarcode((string) $data['ParentContainerBarcode']); } - if (isset($childContainerIds)) { + if (isset($data['ChildContainerIDs'])) { + $childContainerIds = explode(',', $data['ChildContainerIDs']); $container->setChildContainerIds($childContainerIds); + } else { + $container->setChildContainerIds([]); } if (isset($data['Coordinate'])) { $container->setCoordinate((int) $data['Coordinate']); diff --git a/php/optionsendpoint.class.inc b/php/optionsendpoint.class.inc index d985497b..a16e231a 100644 --- a/php/optionsendpoint.class.inc +++ b/php/optionsendpoint.class.inc @@ -134,7 +134,7 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface $userCenters = implode(',',$user->getCenterIDs()); $userProjects = implode(',', $user->getProjectIDs()); - // XXX: This should eventually be replaced by a call directly to a + // TODO: This should eventually be replaced by a call directly to a // Candidate endpoint or Candidate controller that will be able to // provide Candidate Objects. $query = "SELECT c.CandID as id, @@ -143,8 +143,6 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface GROUP_CONCAT(DISTINCT DiagnosisID) as diagnosisIds FROM candidate c LEFT JOIN session s USING (CandID) - LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) - LEFT JOIN biobank_container bc USING (ContainerID) LEFT JOIN candidate_diagnosis_rel USING (CandID) WHERE s.CenterID IN ($userCenters) OR s.ProjectID IN ($userProjects) GROUP BY @@ -185,11 +183,11 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface // Center Endpoint or Center Controller that will be able to provide // Center Objects. $centers = \Utility::getSiteList(); - foreach ($centers as $id => $center) { - if ($user->hasCenter(new \CenterID(strval($id))) === false) { - unset($centers[$id]); - } - } + // foreach ($centers as $id => $center) { + // if ($user->hasCenter(new \CenterID(strval($id))) === false) { + // unset($centers[$id]); + // } + // } // XXX: This should eventually be replaced by a call directly to a // Session Controller or Session Options endpoint that will be able to @@ -205,8 +203,8 @@ class OptionsEndpoint extends \NDB_Page implements RequestHandlerInterface LEFT JOIN biobank_specimen bs ON (s.Id = bs.SessionID) LEFT JOIN biobank_container bc USING (ContainerID) WHERE - s.CenterID IN ($userCenters) OR - bc.CenterID IN ($userCenters)"; + s.CenterID IN ($userCenters) + OR s.ProjectID IN ($userProjects)"; $result = $db->pselect($query, array()); $candidateSessions = []; $sessionCenters = []; diff --git a/php/pooldao.class.inc b/php/pooldao.class.inc index b849eaa0..ff23c53b 100644 --- a/php/pooldao.class.inc +++ b/php/pooldao.class.inc @@ -64,7 +64,7 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance */ public function getInstanceFromId(int $id) : Pool { - return $this->selectInstances([['column' => 'PoolID', 'value'=>$id]])[$id]; + return $this->selectInstances([['column' => 'bp.PoolID', 'value'=>$id]])[$id]; } // XXX: Currently this function is never used with any conditions passed as @@ -88,11 +88,16 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance bp.Quantity, bp.UnitID, GROUP_CONCAT(bspr.SpecimenID) as SpecimenIDs, + GROUP_CONCAT(bc.Barcode) as SpecimenBarcodes, bp.Date, DATE_FORMAT(bp.Time, '%H:%i') as Time FROM biobank_pool bp - LEFT JOIN biobank_specimen_pool_rel bspr - USING (PoolID)"; + LEFT JOIN + biobank_specimen_pool_rel bspr ON bspr.PoolID=bp.PoolID + LEFT JOIN + biobank_specimen bs ON bs.SpecimenID=bspr.SpecimenID + LEFT JOIN + biobank_container bc ON bc.ContainerID=bs.ContainerID"; if (!empty($conditions)) { $whereClause = []; foreach ($conditions as $condition) { @@ -112,11 +117,7 @@ class PoolDAO extends \LORIS\Data\ProvisionerInstance foreach ($poolRows as $id => $poolRow) { $specimenIds = explode(',', $poolRow['SpecimenIDs']); - $specimenBarcodes = []; - foreach ($specimenIds as $specimenId) { - $specimen = $specimenDAO->getInstanceFromId((int) $specimenId); - $specimenBarcodes[] = $specimen->getBarcode(); - } + $specimenBarcodes = explode(',', $poolRow['SpecimenBarcodes']); // set global pool properties based on first specimen of pool $specimenId = (int) $specimenIds[0]; diff --git a/php/specimendao.class.inc b/php/specimendao.class.inc index 407fc1da..f5f431bc 100644 --- a/php/specimendao.class.inc +++ b/php/specimendao.class.inc @@ -58,9 +58,9 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance * @param int $id Value of the $id for the Specimen that will be * instantiated * - * @return Specimen $specimen Specimen Instance. + * @return ?Specimen $specimen Specimen Instance. */ - public function getInstanceFromId(int $id) : Specimen + public function getInstanceFromId(int $id) : ?Specimen { return $this->selectInstances([['column'=>'bs.SpecimenID', 'value'=>$id]])[$id]; } @@ -197,17 +197,7 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance $specimenRows = $this->db->pselectWithIndexKey($query, [], 'SpecimenID'); $specimens = []; foreach ($specimenRows as $id => $specimenRow) { - $parentSpecimenIds = explode(',', $specimenRow['ParentSpecimenIDs'] ?? ''); - $parentSpecimenBarcodes = explode(',', $specimenRow['parentSpecimenBarcodes'] ?? ''); - $projectIds = array_map(fn($id) => new \ProjectID($id), - explode(',', $specimenRow['ProjectIDs'])); - - $specimens[$id] = $this->_getInstanceFromSQL( - $specimenRow, - $projectIds, - $parentSpecimenIds, - $parentSpecimenBarcodes - ); + $specimens[$id] = $this->_getInstanceFromSQL($specimenRow); } return $specimens; @@ -553,9 +543,9 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance } // Prepare project data and sent it to be saved. - foreach ($data['biobank_specimen_project_rel'] as $id => $insert) { - $projdata =& $data['biobank_container_project_rel'][$id]; - $projdata['SpecimenID'] = $specimen->getId(); + foreach ($data['biobank_specimen_project_rel'] as $i => $insert) { + $projInsert =& $data['biobank_specimen_project_rel'][$i]; + $projInsert['SpecimenID'] = $specimen->getId(); } $this->_saveProject($specimen, $data); @@ -663,12 +653,9 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance json_encode($analysis->getData()) : null; } - $containerProjectData = []; - foreach ($container->getProjectIds() as $id) { - $containerProjectData[$id] = [ - 'ContainerID' => $container->getId(), - 'ProjectID' => $id, - ]; + $specimenProjectData = []; + foreach ($specimen->getProjectIds() as $id) { + $specimenProjectData[] = ['ProjectID' => $id]; } return [ @@ -677,7 +664,7 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance 'biobank_specimen_collection' => $collectionData, 'biobank_specimen_preparation' => $preparationData, 'biobank_specimen_analysis' => $analysisData, - 'biobank_container_project_rel' => $containerProjectData, + 'biobank_specimen_project_rel' => $specimenProjectData, ]; } @@ -686,15 +673,11 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance * instantiates it as a Specimen Object. * * @param array $data Values to be converted to array. - * @param array $parentSpecimenIds IDs of the parent Specimen. * * @return Specimen */ private function _getInstanceFromSQL( array $data, - array $projectIds, - array $parentSpecimenIds, - array $parentSpecimenBarcodes, ) : Specimen { $specimen = new Specimen($this->loris); if (isset($data['SpecimenID'])) { @@ -718,18 +701,22 @@ class SpecimenDAO extends \LORIS\Data\ProvisionerInstance if (isset($data['CenterID'])) { $specimen->setCenterId(new \CenterID($data['CenterID'])); } - if (isset($projectIds)) { + if (isset($data['ProjectIDs'])) { + $projectIds = array_map(fn($id) => new \ProjectID($id), + explode(',', $data['ProjectIDs'])); $specimen->setProjectIds($projectIds); } if (isset($data['FreezeThawCycle'])) { $specimen->setFTCycle((int) $data['FreezeThawCycle']); } - if (!empty($parentSpecimenIds)) { + if (!empty($data['ParentSpecimenIDs'])) { + $parentSpecimenIds = explode(',', $data['ParentSpecimenIDs']); $specimen->setParentSpecimenIds($parentSpecimenIds); } else { $specimen->setParentSpecimenIds([]); } - if (!empty($parentSpecimenBarcodes)) { + if (!empty($data['ParentSpecimenBarcodes'])) { + $parentSpecimenBarcodes = explode(',', $data['ParentSpecimenBarcodes']); $specimen->setParentSpecimenBarcodes($parentSpecimenBarcodes); } else { $specimen->setParentSpecimenBarcodes([]); From 5907c6924f7c7c97427f93677495a31f24057c1a Mon Sep 17 00:00:00 2001 From: Rida Abou-Haidar Date: Fri, 8 Sep 2023 16:32:54 -0400 Subject: [PATCH 04/19] fixed site inheritance for batch preparation form --- jsx/batchProcessForm.js | 31 ++++++++----------------------- jsx/specimenTab.js | 12 ++++++------ 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/jsx/batchProcessForm.js b/jsx/batchProcessForm.js index 00ffad33..06b438be 100644 --- a/jsx/batchProcessForm.js +++ b/jsx/batchProcessForm.js @@ -63,7 +63,7 @@ class BatchProcessForm extends React.PureComponent { * */ addListItem(containerId) { - let {list, current, preparation, count} = clone(this.state); + let {list, current, count} = clone(this.state); // Increase count. count++; @@ -74,16 +74,12 @@ class BatchProcessForm extends React.PureComponent { // Set current global values. current.typeId = specimen.typeId; - current.centerId = container.centerId; // Set list values. list[count] = {specimen, container}; - // Set current preparation values. - preparation.centerId = container.centerId; - this.setState( - {preparation, list, current, count, containerId}, + {list, current, count, containerId}, this.setState({containerId: null}) ); } @@ -138,16 +134,7 @@ class BatchProcessForm extends React.PureComponent { Swal.fire('Oops!', 'Specimens must be of the same Type', 'warning'); return Promise.reject(); } - // XXX: This is what the validation used to be. Removed because CBIGR requires - // process from two different sites that are semantically the same. When - // project-level permissions are enabled, this should be restored. - // if (!isEmpty(list) && - // (specimen.typeId !== current.typeId || - // container.centerId !== current.centerId) - // ) { - // swal.fire('Oops!', 'Specimens must be of the same Type and Center', 'warning'); - // return Promise.reject(); - // } + return Promise.resolve(); } @@ -271,18 +258,14 @@ class BatchProcessForm extends React.PureComponent { -

Barcode Input

@@ -321,9 +304,11 @@ class BatchProcessForm extends React.PureComponent { const handleSubmit = () => { const prepList = Object.values(list).map((item) => { const specimen = clone(item.specimen); - specimen.preparation = preparation; + specimen.preparation = clone(preparation); + specimen.preparation.centerId = item.container.centerId; return specimen; }); + console.log(prepList); return new Promise((resolve, reject) => { this.validateList(list) diff --git a/jsx/specimenTab.js b/jsx/specimenTab.js index 3aa59312..e8f2ca25 100644 --- a/jsx/specimenTab.js +++ b/jsx/specimenTab.js @@ -91,9 +91,9 @@ class SpecimenTab extends Component { formatSpecimenColumns(column, value, row) { const {options} = this.props; value = this.mapSpecimenColumns(column, value); - const candId = Object.values(options.candidates) - .find((cand) => cand.pscid == row['PSCID']).id; - const candidatePermission = candId !== undefined; + const candidate = Object.values(options.candidates) + .find((cand) => cand.pscid == row['PSCID']); + const candidatePermission = candidate !== undefined; switch (column) { case 'Barcode': return {value}; @@ -105,15 +105,15 @@ class SpecimenTab extends Component { return {barcodes}; case 'PSCID': if (candidatePermission) { - return {value}; + return {value}; } return {value}; case 'Visit Label': if (candidatePermission) { - const ses = Object.values(options.candidateSessions[candId]).find( + const ses = Object.values(options.candidateSessions[candidate.id]).find( (sess) => sess.label == value ).id; - const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ + const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candidate.id+ '&sessionID='+ses; return {value}; } From 3ddc91b69b8f764348f642dcb104494d69103e57 Mon Sep 17 00:00:00 2001 From: Henri Rabalais Date: Thu, 28 Sep 2023 14:46:03 +0200 Subject: [PATCH 05/19] 24.1+248+250 (#1) * added function (#247) Co-authored-by: Rida Abou-Haidar * fixed site inheritance for batch preparation form * fixes made based on testing --------- Co-authored-by: Rida Abou-Haidar --- jsx/barcodePage.js | 35 +++++++++++++++++--------------- jsx/batchProcessForm.js | 31 ++++++++-------------------- jsx/globals.js | 8 ++++---- jsx/specimenTab.js | 20 ++++++++++-------- php/labelendpoint.class.inc | 14 +++++++++++++ php/specimencontroller.class.inc | 8 +------- 6 files changed, 58 insertions(+), 58 deletions(-) diff --git a/jsx/barcodePage.js b/jsx/barcodePage.js index a5fd433e..b077f159 100644 --- a/jsx/barcodePage.js +++ b/jsx/barcodePage.js @@ -98,26 +98,29 @@ class BarcodePage extends Component { const optcontainer = this.props.options.container; const datacontainers = this.props.data.containers; const parentContainer = datacontainers[container.parentContainerId]; - const dimensions = optcontainer.dimensions[parentContainer.dimensionId]; - let coordinate; - let j = 1; - outerloop: - for (let y=1; y<=dimensions.y; y++) { - for (let x=1; x<=dimensions.x; x++) { - if (j == container.coordinate) { - if (dimensions.xNum == 1 && dimensions.yNum == 1) { - coordinate = x + (dimensions.x * (y-1)); - } else { - const xVal = dimensions.xNum == 1 ? x : String.fromCharCode(64+x); - const yVal = dimensions.yNum == 1 ? y : String.fromCharCode(64+y); - coordinate = yVal+''+xVal; + // if parentContainer is not accessible, this means the user doesn't have access + if (parentContainer) { + const dimensions = optcontainer.dimensions[parentContainer.dimensionId]; + let coordinate; + let j = 1; + outerloop: + for (let y=1; y<=dimensions.y; y++) { + for (let x=1; x<=dimensions.x; x++) { + if (j == container.coordinate) { + if (dimensions.xNum == 1 && dimensions.yNum == 1) { + coordinate = x + (dimensions.x * (y-1)); + } else { + const xVal = dimensions.xNum == 1 ? x : String.fromCharCode(64+x); + const yVal = dimensions.yNum == 1 ? y : String.fromCharCode(64+y); + coordinate = yVal+''+xVal; + } + break outerloop; } - break outerloop; + j++; } - j++; } + return coordinate; } - return coordinate; } /** diff --git a/jsx/batchProcessForm.js b/jsx/batchProcessForm.js index 00ffad33..06b438be 100644 --- a/jsx/batchProcessForm.js +++ b/jsx/batchProcessForm.js @@ -63,7 +63,7 @@ class BatchProcessForm extends React.PureComponent { * */ addListItem(containerId) { - let {list, current, preparation, count} = clone(this.state); + let {list, current, count} = clone(this.state); // Increase count. count++; @@ -74,16 +74,12 @@ class BatchProcessForm extends React.PureComponent { // Set current global values. current.typeId = specimen.typeId; - current.centerId = container.centerId; // Set list values. list[count] = {specimen, container}; - // Set current preparation values. - preparation.centerId = container.centerId; - this.setState( - {preparation, list, current, count, containerId}, + {list, current, count, containerId}, this.setState({containerId: null}) ); } @@ -138,16 +134,7 @@ class BatchProcessForm extends React.PureComponent { Swal.fire('Oops!', 'Specimens must be of the same Type', 'warning'); return Promise.reject(); } - // XXX: This is what the validation used to be. Removed because CBIGR requires - // process from two different sites that are semantically the same. When - // project-level permissions are enabled, this should be restored. - // if (!isEmpty(list) && - // (specimen.typeId !== current.typeId || - // container.centerId !== current.centerId) - // ) { - // swal.fire('Oops!', 'Specimens must be of the same Type and Center', 'warning'); - // return Promise.reject(); - // } + return Promise.resolve(); } @@ -271,18 +258,14 @@ class BatchProcessForm extends React.PureComponent { -

Barcode Input

@@ -321,9 +304,11 @@ class BatchProcessForm extends React.PureComponent { const handleSubmit = () => { const prepList = Object.values(list).map((item) => { const specimen = clone(item.specimen); - specimen.preparation = preparation; + specimen.preparation = clone(preparation); + specimen.preparation.centerId = item.container.centerId; return specimen; }); + console.log(prepList); return new Promise((resolve, reject) => { this.validateList(list) diff --git a/jsx/globals.js b/jsx/globals.js index 1be3388d..3c5aac51 100644 --- a/jsx/globals.js +++ b/jsx/globals.js @@ -291,10 +291,10 @@ function Globals(props) { // Set Parent Container Barcode Value if it exists const parentContainerBarcodeValue = () => { if (container.parentContainerId) { - const barcode = data.containers[ - container.parentContainerId - ].barcode; - return {barcode}; + if (data.containers[container.parentContainerId]) { + return {container.parentContainerBarcode}; + } + return
{container.parentContainerBarcode}
; } }; diff --git a/jsx/specimenTab.js b/jsx/specimenTab.js index 3aa59312..543cdbda 100644 --- a/jsx/specimenTab.js +++ b/jsx/specimenTab.js @@ -89,11 +89,11 @@ class SpecimenTab extends Component { * @return {ReactDOM} */ formatSpecimenColumns(column, value, row) { - const {options} = this.props; + const {data, options} = this.props; value = this.mapSpecimenColumns(column, value); - const candId = Object.values(options.candidates) - .find((cand) => cand.pscid == row['PSCID']).id; - const candidatePermission = candId !== undefined; + const candidate = Object.values(options.candidates) + .find((cand) => cand.pscid == row['PSCID']); + const candidatePermission = candidate !== undefined; switch (column) { case 'Barcode': return {value}; @@ -105,15 +105,15 @@ class SpecimenTab extends Component { return {barcodes}; case 'PSCID': if (candidatePermission) { - return {value}; + return {value}; } return {value}; case 'Visit Label': if (candidatePermission) { - const ses = Object.values(options.candidateSessions[candId]).find( + const ses = Object.values(options.candidateSessions[candidate.id]).find( (sess) => sess.label == value ).id; - const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candId+ + const visitLabelURL = loris.BaseURL+'/instrument_list/?candID='+candidate.id+ '&sessionID='+ses; return {value}; } @@ -138,7 +138,11 @@ class SpecimenTab extends Component { case 'Projects': return {value.join(', ')}; case 'Container Barcode': - return {value}; + // check if container has be queried + if (Object.values(data.containers).find(container => container.barcode == value)) { + return {value}; + } + return {value}; default: return {value}; } diff --git a/php/labelendpoint.class.inc b/php/labelendpoint.class.inc index 159cb47a..a53941e9 100644 --- a/php/labelendpoint.class.inc +++ b/php/labelendpoint.class.inc @@ -57,6 +57,20 @@ class LabelEndpoint implements RequestHandlerInterface ]; } + /** + * This function can be overridden in a module's page to load the necessary + * resources to check the permissions of a user. + * + * @param User $user The user to load the resources for + * @param ServerRequestInterface $request The PSR15 Request being handled + * + * @return void + */ + public function loadResources( + \User $user, ServerRequestInterface $request + ) : void { + } + /** * This function passes the request to the handler. This is necessary since * the Endpoint bypass the Module class. diff --git a/php/specimencontroller.class.inc b/php/specimencontroller.class.inc index 973fd71b..8e0b7811 100644 --- a/php/specimencontroller.class.inc +++ b/php/specimencontroller.class.inc @@ -71,7 +71,6 @@ class SpecimenController foreach ($specimenIt as $id => $specimen) { $specimens[$id] = $specimen; } - return $specimens; } @@ -177,12 +176,7 @@ class SpecimenController */ private function _getDataProvisioner() : SpecimenDAO { - $dao = new SpecimenDAO($this->loris); - - if ($this->user->hasPermission('access_all_profiles') === false) { - $dao = $dao->filter(new \LORIS\Data\Filters\UserSiteMatch()); - } - return $dao; + return new SpecimenDAO($this->loris); } /** From bbb553e38b25b7b7eb5ec7e7f8cf8e591eddda4b Mon Sep 17 00:00:00 2001 From: Henri Rabalais Date: Fri, 29 Sep 2023 07:14:45 -0400 Subject: [PATCH 06/19] started adding jsdocs --- jsx/globals.js | 7 ++-- jsx/search.js | 102 +++++++++++++++++++++++++------------------------ 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/jsx/globals.js b/jsx/globals.js index 3c5aac51..6aab2ebc 100644 --- a/jsx/globals.js +++ b/jsx/globals.js @@ -290,11 +290,12 @@ function Globals(props) { if (loris.userHasPermission('biobank_container_view')) { // Set Parent Container Barcode Value if it exists const parentContainerBarcodeValue = () => { - if (container.parentContainerId) { + if (container.parentContainerBarcode) { + const barcode = container.parentContainerBarcode if (data.containers[container.parentContainerId]) { - return {container.parentContainerBarcode}; + return {barcode}; } - return
{container.parentContainerBarcode}
; + return
{barcode}
; } }; diff --git a/jsx/search.js b/jsx/search.js index ba1fdad4..0776510a 100644 --- a/jsx/search.js +++ b/jsx/search.js @@ -1,62 +1,66 @@ -import React, {PureComponent} from 'react'; +import React, { useState } from 'react'; import Modal from 'Modal'; /** - * Provides a modal window that can be used to search barcodes + * Provides a modal window that can be used to search barcodes. + * + * @component + * @param {Object} props - The component's props. + * @param {string} props.title - The title for the modal. + * @param {boolean} props.show - A flag to control the visibility of the modal. + * @param {Function} props.onClose - A callback function to close the modal. + * @param {Object} props.barcodes - An object containing barcode data. + * @param {Object} props.history - React Router history object for navigation. + * @returns {JSX.Element} The rendered component. */ -class Search extends PureComponent { +function Search({ + title, + show, + onClose, + barcodes = {}, + history, +}) { /** - * Constructor + * State for storing the barcode value. + * + * @type {string | null} */ - constructor() { - super(); - this.state = {barcode: null}; - } + const [barcode, setBarcode] = useState(null); /** - * Render React component + * Handles user input in the barcode textbox. * - * @return {ReactDOM} + * @param {string} name - The name of the input element. + * @param {string} value - The new value of the barcode. */ - render() { - const onInput = (name, value) => { - this.setState({barcode: value}); - if (Object.values(this.props.barcodes).find( - (barcode) => barcode == value) - ) { - this.props.history.push(`/barcode=${value}`); - this.props.onClose(); - } - }; - return ( - - - - - - ); - } -} - -Search.propTypes = { + const onInput = (name, value) => { + setBarcode(value); + if (Object.values(barcodes).find((barcode) => barcode === value)) { + history.push(`/barcode=${value}`); + onClose(); + } + }; -}; - -Search.defaultProps = { - -}; + return ( + + + + + + ); +} export default Search; From 158fc78ac6b5bd8bcae8a12467a19ca3b9ded5be Mon Sep 17 00:00:00 2001 From: Henri Rabalais Date: Mon, 13 Nov 2023 07:01:40 -0500 Subject: [PATCH 07/19] refactoring jsx to typescript --- jsx/.specimen.js.swp | Bin 0 -> 20480 bytes jsx/Shipment.js | 112 ++++++------ jsx/{container.js => container.tsx} | 111 ++++++------ jsx/filter.js | 168 +++++++++-------- jsx/header.js | 212 ---------------------- jsx/header.tsx | 270 ++++++++++++++++++++++++++++ jsx/{search.js => search.tsx} | 33 ++-- jsx/{specimen.js => specimen.tsx} | 127 +++++++++---- 8 files changed, 581 insertions(+), 452 deletions(-) create mode 100644 jsx/.specimen.js.swp rename jsx/{container.js => container.tsx} (67%) delete mode 100644 jsx/header.js create mode 100644 jsx/header.tsx rename jsx/{search.js => search.tsx} (61%) rename jsx/{specimen.js => specimen.tsx} (57%) diff --git a/jsx/.specimen.js.swp b/jsx/.specimen.js.swp new file mode 100644 index 0000000000000000000000000000000000000000..1df49f760ce85b98f03901354dbb2f9d67bee769 GIT binary patch literal 20480 zcmeI2dx#xZ9mgkW(svSTTTu}`SxfHjX7Ai28mXJTZIa!DEKRygUIfx4&fYnjoz3jr z$;{l{Y;PB#4+%|8Q^is=7E-F!np9enf>1$=7E51(wEBlu5sFwJNUTLg#P9FSIcH|> zy;-9M6gdMQ?w#}eo!|MLGw1OeTeW+58@t(FZQ*&3WxY7k&(FQF$ojR5nzP$wHfDeElTx(ek z&;>pO7Jzpl$J^i+;C1jKcn+KfkAp|R{h$dvKyvQ^W$;0;3|tMa1XqAxqiyHFGvH}( z2HXek27AFCupSHm27YxF(t~e;GvGA%8fbtB+y=IQHNXbTz^hkU)+^vCa2Sk%o#1kC z?h4C#5}X2G0uO^PfDr5eL*SF({owrNmh~F=J~#`W0FQ!4z~{l|z%W<~7J~0Bwyfje zD7XjQ4Fa$m^n(wBU*h2ANpKR3gO%VN9M-%Ao&{eAkAX+Qm%#(zPH+Hh0ylzpa5(fs z@GN*7d7#Dm0cOT+qeskrMU&X~`23G7xKD+{eQ zbx1u-ym}B<4tvCYL>yZl*tS(UC+#SH9_ z?kuCJS~l0CDIw_q{oCYoc)BFFAf$%IBy1dktjfAcvuP1M>tw?QDy}z8Dbr_w-Zl&k zF%pp!ZYT9!VaTIFC+6KfbFImXxfvUXLnmN0--+Yfohe=|9rS0z2@hg0{jVJQjkr`P zR+`V_xNcciUt;xO(D!PGs!fE-*3PM{ByBz;>gGOEJylL$y}+p@-ZU?zxza#V2DLcV zPgXQ(sfSf>VW=NHltpw)fwba;%aPrPd9=leM|m{m#W75$yCl7i4k$4WG(y)&c&Vp{ zDLY%P2U`3oa&Pax&UaR|?j(Bkrr+IvyFx94@;rr{Tlcki(T%_!F z#6#by@$Sn0J7y|`$um`2U#PPD=IRhg7KPG)^xZPE-8*Q0(` z3hQ2w@TfGGPnC_1t`Hqoj=X~tNtdp3z>$BY22(?&?r*c2)S^-~)r905d{j4@4qa`_ zO)>5-sDj)`WZ7_QtBl>s$i7&AafIY5)MKP`YuhKrtjNyu1P^MSU&=}ov&)6FbMXVx zA(_>9lruoC8AZLN@mlsivS^wZ`ufWvk#^}*(swmMUkSy zSoB_R#bUi|z3l7ec8Kx=lPU7s^ zzO|qFEZm8CE4Je#@`8i57jNf>8(zfSK5Jv@UAjX#mQpK3Vst)69H~}nw(!v$sG!ue z`fPR?kYU0)R3}wSPT=_Hpnj$*A%S2w_&(A6n0{DPTpzW9CoQjo=^}|D)r>{|X>_aK zd=6N9;&=g%L}{{V^j})G$Rn05DmSgI=)W2bRytr>v_oSGBKKAp6K0(nPs~z-mxeYc z&6=m{7lzMO#A6sURL_zLuGL_(;*%dKsx?AY*?`%J>>;$rqr~Ge1_b)wr9ndF>QR#w zEQ-|O#p?d8H!ARwK2)G;EF0A09&0@{@JVg4>_g@Svf8buX<`X_OWq0!==&P0;C4 zz6#4SkgVx+mKIAwl&LkjLep^>BC?`a10;kYCdRS6l3U)ZWX0i#Wz?aoQdMv6%%zY@9=GI@$&f6S?42D6 z&Phc&IyuOq=fwX1THNC_um9BfYaasKt2E)K|fdmevbY9S#SzG3?2e>CU6|w55~X&KxY9x zzyg26xdr(NJOLV@4yJ$)Ccs*-23!NkAK*Oh_kRyw1INK${RE^q^Q0r&ZI*FOmy zuovtGyTB@NJy-<(g1h~nfM>vi;69LmgJ3_{19pJz;41JZ-0wdR?gh7lEnpCA1U7gd z_$%)8-vp0=NpJ^P0e*vf{IlRIU>bN}4D1K{z*a!_{T~5~!RzSDv)}}n1}@kP7@&S$ z0N%#E{>$Jc@I^2J%<~^>faa0(QNvqLOO}e!hwpk)&}j;;5LxSu>8^&zx6@3;*(@WD);c}=JA|*I1 zkjD;Ptrt8TJEw*#pAT2ILgH+*T-+U2o5lBR&z0NFRhzk&LJGaLLxQ$VoizInUa#qW zH+G`J=OZgouHD)kQ!_6UM74RyskL0Tv&~9WD@8OxUc+Z``EwIEg65{ITpH~jT5uJW zD3^?a*2#2ZwkzsI|7`AJljFZ~y=R literal 0 HcmV?d00001 diff --git a/jsx/Shipment.js b/jsx/Shipment.js index f1ea44e1..c2b0d1c1 100644 --- a/jsx/Shipment.js +++ b/jsx/Shipment.js @@ -1,13 +1,11 @@ -import {useState} from 'react'; -import {get, post} from './helpers.js'; +import React, { useState } from 'react'; +import { get, post } from './helpers.js'; /** - * React effect for creating a request to create a new - * shipment + * Custom hook for managing shipment data. * - * @param {object} initShipment - the initial value for the shipment - * - * @return {Shipment} + * @param {Shipment} initShipment - Initial shipment data. + * @returns {Object} - Shipment state and functions to modify it. */ export function UseShipment(initShipment = {}) { const [init, setInit] = useState(initShipment); @@ -49,33 +47,28 @@ export function UseShipment(initShipment = {}) { } /** - * A Shipment of a container + * Represents a shipment of containers. */ +interface ShipmentData { + id?: string; + barcode?: string; + type?: string; + destinationCenterId?: number; + logs?: LogData[]; + containerIds?: string[]; +} + class Shipment { /** - * Constructor - * - * @param {string} id - shipment id - * @param {string} barcode - shipment barcode - * @param {string} type - shipment type - * @param {int} destinationCenterId - shipment destination - * @param {array} logs - logs for this shipment - * @param {array} containerIds - containers in this shipment + * @param {ShipmentData} data */ - constructor({ - id = null, - barcode = null, - type = null, - destinationCenterId = null, - logs = [], - containerIds = [], - }) { - this.id = id; - this.barcode = barcode; - this.type = type; - this.destinationCenterId = destinationCenterId; - this.logs = logs.map((log) => new Log(log)); - this.containerIds = containerIds; + constructor(data: ShipmentData = {}) { + this.id = data.id || null; + this.barcode = data.barcode || null; + this.type = data.type || null; + this.destinationCenterId = data.destinationCenterId || null; + this.logs = data.logs ? data.logs.map(log => new Log(log)) : []; + this.containerIds = data.containerIds || []; } /** @@ -144,35 +137,52 @@ class Shipment { } /** - * A log of shipments + * Represents a shipment log */ +interface LogData { + barcode?: string; + centerId?: string; + status?: string; + user?: string; + temperature?: number; + date?: string; + time?: string; + comments?: string; +} + class Log { + barcode: string | null; + centerId: string | null; + status: string | null; + user: string | null; + temperature: number | null; + date: string | null; + time: string | null; + comments: string | null; + /** - * Constructor - * - * @param {object} props - React props + * @param {LogData} data */ - constructor(props = {}) { - this.barcode = props.barcode || null; - this.centerId = props.centerId || null; - this.status = props.status || null; - this.user = props.user || null; - this.temperature = props.temperature || null; - this.date = props.date || null; - this.time = props.time || null; - this.comments = props.comments || null; + constructor(data: LogData = {}) { + this.barcode = data.barcode || null; + this.centerId = data.centerId || null; + this.status = data.status || null; + this.user = data.user || null; + this.temperature = data.temperature || null; + this.date = data.date || null; + this.time = data.time || null; + this.comments = data.comments || null; } /** - * Set a value - * - * @param {string} name - the key - * @param {?} value - the value - * - * @return {Log} - A new log object with the key set to value + * Sets name to value in this shipment. + * + * @param {string} name - The key. + * @param {any} value - The value. + * @returns {Log} - Updated shipment. */ - set(name, value) { - return new Log({...this, [name]: value}); + set(name: keyof LogData, value: string | number | null): Log { + return new Log({ ...this, [name]: value }); } } diff --git a/jsx/container.js b/jsx/container.tsx similarity index 67% rename from jsx/container.js rename to jsx/container.tsx index 4bc3ea0a..847f2e3a 100644 --- a/jsx/container.js +++ b/jsx/container.tsx @@ -1,47 +1,56 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import {Link} from 'react-router-dom'; - -import {mapFormOptions} from './helpers.js'; +import { Link } from 'react-router-dom'; +import { mapFormOptions } from './helpers.js'; import ContainerDisplay from './containerDisplay'; -/** - * Biobank Container - * - * Fetches data corresponding to a given Container from Loris backend and - * displays a page allowing viewing of meta information of the container - * - * @author Henri Rabalais - * @version 1.0.0 - * - * */ -class BiobankContainer extends Component { - /** - * Constructor - */ - constructor() { - super(); - this.drag = this.drag.bind(this); - } - /** - * Handle dragging of a container - * - * @param {Event} e - the drag event - */ - drag(e) { - const container = JSON.stringify(this.props.data.containers[e.target.id]); +type ContainerProps = { + current = object, + data = object, + editable = object, + options = object, + container = object, + getParentContainerBarcodes = Function, + getBarcodePathDisplay = Function, + history = string, + edit = Function, + clearAll = Function, + setCurrent = Function, + setCheckoutList = Function, + editContainer = Function, + updateContainer = Function, +} + +/** + * Container Component for displaying container information and actions. + * + * @component + * @param {ContainerProps} props - The properties passed to the component. + * @returns {ReactElement} React element representing the Container. + */ +const BiobankContainer = ({ + current, + data, + editable, + options, + container, + getParentContainerBarcodes, + getBarcodePathDisplay, + history, + edit, + clearAll, + setCurrent, + setCheckoutList, + editContainer, + updateContainer, +}) => { + const drag = useCallback((e) => { + const container = JSON.stringify(data.containers[e.target.id]); e.dataTransfer.setData('text/plain', container); - } + }, [data.containers]); - /** - * Render React component - * - * @return {JSX} - */ render() { - const {current, data, editable, options, container} = this.props; - const checkoutButton = () => { if (!(loris.userHasPermission('biobank_container_update')) || (data.containers[container.id].childContainerIds.length == 0)) { @@ -54,9 +63,7 @@ class BiobankContainer extends Component { className={!editable.containerCheckout && !editable.loadContainer ? 'action-button update open' : 'action-button update closed'} title='Checkout Child Containers' - onClick={()=>{ - this.props.edit('containerCheckout'); - }} + onClick={() => edit('containerCheckout'}; >
@@ -64,7 +71,7 @@ class BiobankContainer extends Component { ); }; - const parentBarcodes = this.props.getParentContainerBarcodes(container); + const parentBarcodes = getParentContainerBarcodes(container); const barcodes = mapFormOptions(data.containers, 'barcode'); // delete values that are parents of the container Object.keys(parentBarcodes) @@ -75,7 +82,7 @@ class BiobankContainer extends Component { ) ); - const barcodePathDisplay = this.props.getBarcodePathDisplay(parentBarcodes); + const barcodePathDisplay = getBarcodePathDisplay(parentBarcodes); const coordinates = data.containers[container.id].childContainerIds .reduce((result, id) => { const container = data.containers[id]; @@ -89,7 +96,7 @@ class BiobankContainer extends Component {
{checkoutButton()}
{barcodePathDisplay} @@ -136,7 +143,7 @@ class BiobankContainer extends Component {
); - const coordinate = this.props.getCoordinateLabel(child); + const coordinate = getCoordinateLabel(child); coordinateList.push(
at {coordinate}
); } else { listUnassigned.push( @@ -185,8 +192,4 @@ class BiobankContainer extends Component { } } -BiobankContainer.propTypes = { - containerPageDataURL: PropTypes.string.isRequired, -}; - export default BiobankContainer; diff --git a/jsx/filter.js b/jsx/filter.js index 4cf41d0e..3ff44945 100644 --- a/jsx/filter.js +++ b/jsx/filter.js @@ -1,8 +1,5 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; - -import {Tabs, TabPane} from 'Tabs'; - +import React from 'react'; +import { Tabs, TabPane } from 'Tabs'; import SpecimenTab from './specimenTab'; import ContainerTab from './containerTab'; import PoolTab from './poolTab'; @@ -10,98 +7,95 @@ import ShipmentTab from './shipmentTab'; /** * Render a filter in the biobank. + * @component + * @param {BiobankFilterProps} props - The props for the component. + * @returns {JSX.Element} The rendered component. */ -class BiobankFilter extends Component { - /** - * Render the component - * - * @return {JSX} - */ - render() { - const specimenTab = ( - - ); +const BiobankFilter = ({ + data, + options, + saveBatchEdit, + createPool, + createSpecimens, + updateSpecimens, + editSpecimens, + history, + increaseCoordinate, + loading, +}) => { + const specimenTab = ( + + ); - const containerTab = ( - - ); + const containerTab = ( + + ); - const poolTab = ( - - ); + const poolTab = ( + + ); - const shipmentTab = ( - - ); + const shipmentTab = ( + + ); - const tabInfo = []; - const tabList = []; - if (loris.userHasPermission('biobank_specimen_view')) { - tabInfo.push({id: 'specimens', content: specimenTab}); - tabList.push({id: 'specimens', label: 'Specimens'}); - } - if (loris.userHasPermission('biobank_container_view')) { - tabInfo.push({id: 'containers', content: containerTab}); - tabList.push({id: 'containers', label: 'Containers'}); - } - if (loris.userHasPermission('biobank_pool_view')) { - tabInfo.push({id: 'pools', content: poolTab}); - tabList.push({id: 'pools', label: 'Pools'}); - } - tabInfo.push({id: 'shipments', content: shipmentTab}); - tabList.push({id: 'shipments', label: 'Shipments'}); + const tabInfo = []; + const tabList = []; - const tabContent = Object.keys(tabInfo).map((key) => { - return ( - - {tabInfo[key].content} - - ); - }); + if (loris.userHasPermission('biobank_specimen_view')) { + tabInfo.push({ id: 'specimens', content: specimenTab }); + tabList.push({ id: 'specimens', label: 'Specimens' }); + } - return ( -
- - {tabContent} - -
- ); + if (loris.userHasPermission('biobank_container_view')) { + tabInfo.push({ id: 'containers', content: containerTab }); + tabList.push({ id: 'containers', label: 'Containers' }); } -} -BiobankFilter.propTypes = { - data: PropTypes.object.isRequired, - options: PropTypes.object.isRequired, -}; + if (loris.userHasPermission('biobank_pool_view')) { + tabInfo.push({ id: 'pools', content: poolTab }); + tabList.push({ id: 'pools', label: 'Pools' }); + } + + tabInfo.push({ id: 'shipments', content: shipmentTab }); + tabList.push({ id: 'shipments', label: 'Shipments' }); + + const tabContent = Object.keys(tabInfo).map((key) => ( + + {tabInfo[key].content} + + )); -BiobankFilter.defaultProps = { + return ( +
+ + {tabContent} + +
+ ); }; export default BiobankFilter; diff --git a/jsx/header.js b/jsx/header.js deleted file mode 100644 index c0d967e8..00000000 --- a/jsx/header.js +++ /dev/null @@ -1,212 +0,0 @@ -import React, {Component} from 'react'; - -import Modal from 'Modal'; -import LifeCycle from './lifeCycle.js'; -import SpecimenForm from './specimenForm.js'; - -import Swal from 'sweetalert2'; - -/** - * React component to display a header. - */ -class Header extends Component { - /** - * Render react component - * - * @return {JSX} - */ - render() { - const {options, container, specimen, editable, current} = this.props; - const updateContainer = () => - Promise.resolve( - this.props.updateContainer(current.container) - ); - - const status = options.container.stati[container.statusId].label; - const renderActionButton = () => { - if (status == 'Available' && - specimen.quantity > 0 && - !specimen.poolId) { - const openAliquotForm = () => this.props.edit('aliquotForm'); - return ( -
- + -
- ); - } else { - return
+
; - } - }; - const addAliquotForm = () => { - if (specimen && loris.userHasPermission('biobank_specimen_create')) { - return ( -
-
- {renderActionButton()} -
- -
- ); - } - }; - - const alterLotNumber = () => { - if (loris.userHasPermission('biobank_specimen_alter')) { - return ( -
- { - this.props.edit('lotForm'); - this.props.editContainer(this.props.container); - }} - /> -
- ); - } - }; - - const alterExpirationDate = () => { - if (loris.userHasPermission('biobank_specimen_alter')) { - return ( -
- { - this.props.edit('expirationForm'); - this.props.editContainer(this.props.container); - }} - /> -
- ); - } - }; - - const lotForm = ( - - - - - - ); - - const expirationForm = ( - - - - - - ); - - const parentBarcodes = this.props.getParentContainerBarcodes(container); - const barcodePathDisplay = this.props.getBarcodePathDisplay(parentBarcodes); - const printBarcode = () => { - const labelParams = [{ - barcode: container.barcode, - type: options.specimen.types[specimen.typeId].label, - }]; - this.props.printLabel(labelParams) - .then(() => (Swal.fire('Print Barcode Number: ' + container.barcode))); - }; - - return ( -
-
-
- Barcode -
- {container.barcode} -
- - Address: {barcodePathDisplay}
- Lot Number: {container.lotNumber} {alterLotNumber()}
- Expiration Date: {container.expirationDate}{alterExpirationDate()} -
- {lotForm} - {expirationForm} -
-
-
- -
-
- {addAliquotForm()} - -
- -
- ); - } -} - -/** - * Biobank Container Checkout - * - * @param {object} props - * @return {*} - **/ -function ContainerCheckout(props) { - const checkoutContainer = () => { - props.editContainer(props.container) - .then(() => props.setContainer('parentContainerId', null)) - .then(() => props.setContainer('coordinate', null)) - .then(() => props.updateContainer()); - }; - - return (loris.userHasPermission('biobank_container_update') && - props.container.parentContainerId) ? ( -
-
- -
-
- ) : null; -} - -export default Header; diff --git a/jsx/header.tsx b/jsx/header.tsx new file mode 100644 index 00000000..c2fb2998 --- /dev/null +++ b/jsx/header.tsx @@ -0,0 +1,270 @@ +import React, { FC, ReactElement } from 'react'; +import Modal from 'Modal'; +import LifeCycle from './lifeCycle.js'; +import SpecimenForm from './specimenForm.js'; +import Swal from 'sweetalert2'; +import { FormElement, TextboxElement, DateElement } from 'Form'; + +declare const loris: any; + + +// TODO: Replace 'any' and 'Function' with appropriate declarations +/* + * Props for the Header Component + */ +interface HeaderProps { + data: any; + options: any; + container: any; + specimen: any; + editable: any; + current: any; + updateContainer: Function; + edit: Function; + setSpecimen: Function; + createSpecimens: Function; + setContainer: Function; + editContainer: Function; + clearAll: Function; + increaseCoordinate: Function; + getParentContainerBarcodes: Function; + getBarcodePathDisplay: Function; + printLabel: Function; +} + +/** + * Header component for displaying specimen information and actions. + * + * @component + * @param {HeaderProps} props - The properties passed to the component. + * @returns {ReactElement} React element representing the header. + */ +const Header = ({ + data, + options, + container, + specimen, + editable, + current, + updateContainer, + edit, + setSpecimen, + createSpecimens, + setContainer, + editContainer, + clearAll, + increaseCoordinate, + getParentContainerBarcodes, + getBarcodePathDisplay, + printLabel, +}: HeaderProps): ReactElement => { + const status = options.container.stati[container.statusId].label; + + /** + * Renders an action button for adding aliquots based on specimen and container status. + * + * @function + * @returns {ReactElement} Action button for adding aliquots. + */ + const renderActionButton = (): ReactElement => { + if (status === 'Available' && specimen.quantity > 0 && !specimen.poolId) { + const openAliquotForm = () => edit('aliquotForm'); + return ( +
+ + +
+ ); + } else { + return
+
; + } + }; + + /** + * Generates the form for adding aliquots based on user permissions and specimen data. + * + * @function + * @returns {ReactElement|null} Aliquot form or null if the user doesn't have permission. + */ + const addAliquotForm = (): ReactElement => { + if (specimen && loris.userHasPermission('biobank_specimen_create')) { + return ( +
+
+ {renderActionButton()} +
+ +
+ ); + } + }; + + /** + * Handles editing the lot number of the container. + * + * @component + * @returns {ReactElement|null} Edit lot number button or null if the user doesn't have permission. + */ + const AlterLotNumberButton = (): ReactElement => { + if (loris.userHasPermission('biobank_specimen_alter')) { + return ( +
+ { + edit('lotForm'); + editContainer(container); + }} + /> +
+ ); + } + }; + + /** + * Handles editing the expiration date of the container. + * + * @component + * @returns {ReactElement|null} Edit expiration date button or null if the user doesn't have permission. + */ + const AlterExpirationDateButton = (): ReactElement => { + if (loris.userHasPermission('biobank_specimen_alter')) { + return ( +
+ { + edit('expirationForm'); + editContainer(container); + }} + /> +
+ ); + } + }; + + const lotForm = ( + + + + + + ); + + const expirationForm = ( + + + + + + ); + + const parentBarcodes = getParentContainerBarcodes(container); + const barcodePathDisplay = getBarcodePathDisplay(parentBarcodes); + + /** + * Prints the barcode of the container and its type. + * + * @function + * @returns {void} + */ + const printBarcode = (): void => { + const labelParams = [ + { + barcode: container.barcode, + type: options.specimen.types[specimen.typeId].label, + }, + ]; + printLabel(labelParams).then(() => + Swal.fire('Print Barcode Number: ' + container.barcode) + ); + }; + + /** + * Handles the container checkout action. + * + * @function + * @returns {void} + */ + const checkoutContainer = (): void => { + editContainer(container) + .then(() => setContainer('parentContainerId', null)) + .then(() => setContainer('coordinate', null)) + .then(() => updateContainer()); + }; + + return ( +
+
+
+ Barcode +
+ {container.barcode} +
+ + Address: {barcodePathDisplay}
+ Lot Number: {container.lotNumber} +
+ Expiration Date: {container.expirationDate} + +
+ {lotForm} + {expirationForm} +
+
+
+ +
+
+ {addAliquotForm()} + {loris.userHasPermission('biobank_container_update') && + container.parentContainerId ? ( +
+
+ +
+
+ ) : null} +
+ +
+ ); +}; + +export default Header; diff --git a/jsx/search.js b/jsx/search.tsx similarity index 61% rename from jsx/search.js rename to jsx/search.tsx index 0776510a..f1eced9f 100644 --- a/jsx/search.js +++ b/jsx/search.tsx @@ -1,39 +1,48 @@ -import React, { useState } from 'react'; +import React, { useState, ReactElement } from 'react'; import Modal from 'Modal'; +import {FormElement, TextboxElement} from 'Form'; + +/** + * Props for the Search component. + */ +interface SearchProps { + title: string; + show: boolean; + onClose: () => void; + barcodes?: {[key: string]: string}; + history: any; // TODO: Replace 'any' with appropriate type for history object +} /** * Provides a modal window that can be used to search barcodes. * * @component - * @param {Object} props - The component's props. - * @param {string} props.title - The title for the modal. - * @param {boolean} props.show - A flag to control the visibility of the modal. - * @param {Function} props.onClose - A callback function to close the modal. - * @param {Object} props.barcodes - An object containing barcode data. - * @param {Object} props.history - React Router history object for navigation. - * @returns {JSX.Element} The rendered component. + * @param {SearchProps} props - The component's props. + * @returns {ReactElement} The rendered component. */ -function Search({ +const Search = ({ title, show, onClose, barcodes = {}, history, -}) { +}: SearchProps): ReactElement => { /** * State for storing the barcode value. * * @type {string | null} */ - const [barcode, setBarcode] = useState(null); + const [barcode, setBarcode] = useState(null); /** * Handles user input in the barcode textbox. * + * @function * @param {string} name - The name of the input element. * @param {string} value - The new value of the barcode. + * @returns {void} */ - const onInput = (name, value) => { + const onInput = (name: string, value: string) : void => { setBarcode(value); if (Object.values(barcodes).find((barcode) => barcode === value)) { history.push(`/barcode=${value}`); diff --git a/jsx/specimen.js b/jsx/specimen.tsx similarity index 57% rename from jsx/specimen.js rename to jsx/specimen.tsx index 9d9ce3e8..b0963148 100644 --- a/jsx/specimen.js +++ b/jsx/specimen.tsx @@ -1,28 +1,60 @@ import React from 'react'; import PropTypes from 'prop-types'; import SpecimenProcessForm from './processForm'; - import {clone} from './helpers.js'; -/** - * Biobank Specimen - * - * @param {object} props the props! - * @return {*} - */ -function BiobankSpecimen(props) { - const {current, editable, errors, options, specimen, container} = props; +// TODO: Replace 'any' and 'Function' with appropriate declarations +type SpecimenProps = { + current = any, + editable = any, + errors = any, + options = any, + specimen = any, + container = any, + editSpecimen = Function, + edit = Function, + clearAll = Function, + editable = boolean, + process = 'collection' | 'preparation' | 'analysis', + setCurrent = Function, + setSpecimen = Function, + updateSpecimen = Function, +} + +/** + * Specimen Component for displaying specimen information and actions. + * + * @component + * @param {SpecimenProps} props - The properties passed to the component. + * @returns {ReactElement} React element representing the Specimen. + */ +const BiobankSpecimen = ({ + current, + editable, + errors, + options, + specimen, + container, + editSpecimen, + edit, + clearAll, + editable, + process, + setCurrent, + setSpecimen, + updateSpecimen, + }: SpecimenProps) => { const addProcess = async (process) => { const newSpecimen = clone(specimen); newSpecimen[process] = {centerId: container.centerId}; - await props.editSpecimen(newSpecimen); - props.edit(process); + await editSpecimen(newSpecimen); + edit(process); }; const alterProcess = (process) => { - props.editSpecimen(specimen) - .then(() => props.edit(process)); + editSpecimen(specimen) + .then(() => edit(process)); }; return ( @@ -32,13 +64,13 @@ function BiobankSpecimen(props) { alterProcess={alterProcess} specimen={specimen} editable={editable} - clearAll={props.clearAll} + clearAll={clearAll} current={current} errors={errors} options={options} - setCurrent={props.setCurrent} - setSpecimen={props.setSpecimen} - updateSpecimen={props.updateSpecimen} + setCurrent={setCurrent} + setSpecimen={setSpecimen} + updateSpecimen={updateSpecimen} > @@ -48,16 +80,12 @@ function BiobankSpecimen(props) { ); } -BiobankSpecimen.propTypes = { - specimenPageDataURL: PropTypes.string.isRequired, -}; - /** * React component to display processes * + * @component * @param {object} props - React props - * - * @return {ReactDOM[]} + * @returns {ReactElement} React element */ function Processes(props) { return React.Children.map(props.children, (child) => { @@ -65,22 +93,50 @@ function Processes(props) { }); } +// TODO: Replace 'any' and 'Function' with appropriate declarations +type ProcessPanelProps = { + editable = any, + process = 'collection' | 'preparation' | 'analysis', + current = any, + specimen = any, + options = any, + alterProcess = Function, + clearAll = Function, + addProcess, + errors = any, + setCurrent = Function, + setSpecimen = Function, + updateSpecimen = Function, +} + /** * React component to display a panel of processes * - * @param {object} props - React props - * - * @return {ReactDOM} + * @component + * @param {ProcessPanelProps} + * @return {ReactElement} */ -function ProcessPanel(props) { - const {editable, process, current, specimen, options} = props; +const ProcessPanel({ + editable, + process, + current, + specimen, + options, + alterProcess, + clearAll, + addProcess, + errors, + setCurrent, + setSpecimen, + updateSpecimen, + }: ProcessPanelProps) { const alterProcess = () => { if (loris.userHasPermission('biobank_specimen_alter')) { return ( props.alterProcess(process)} + onClick={editable[process] ? null : () => alterProcess(process)} /> ); } @@ -92,7 +148,7 @@ function ProcessPanel(props) { Cancel @@ -113,10 +169,9 @@ function ProcessPanel(props) { !specimen[process] && !editable[process] && loris.userHasPermission('biobank_specimen_update')) { - const addProcess = () => props.addProcess(process); panel = (
-
+
addProcess(process)}>
ADD {process.toUpperCase()}
@@ -128,7 +183,7 @@ function ProcessPanel(props) { ); From 13e132ae7456c874256438a34b2b09854cbbd891 Mon Sep 17 00:00:00 2001 From: Henri Rabalais Date: Fri, 19 Jan 2024 07:50:59 -0500 Subject: [PATCH 08/19] reorganized directory structure --- jsx/.specimen.js.swp | Bin 20480 -> 0 bytes jsx/APIs/BaseAPI.ts | 202 +++++ jsx/APIs/ContainerAPI.ts | 10 + jsx/APIs/LabelAPI.ts | 10 + jsx/APIs/OptionAPI.ts | 10 + jsx/APIs/PoolAPI.ts | 10 + jsx/APIs/ShipmentAPI.ts | 10 + jsx/APIs/SpecimenAPI.ts | 10 + jsx/APIs/index.js | 9 + jsx/Shipment.js | 189 ---- jsx/barcodePage.js | 424 --------- jsx/batchProcessForm.js | 384 -------- jsx/biobankIndex.js | 840 ------------------ .../BatchEditForm.js} | 19 +- jsx/components/BatchProcessForm.tsx | 445 ++++++++++ jsx/components/BiobankIndex.js | 129 +++ .../ContainerDisplay.js} | 11 +- .../ContainerForm.js} | 46 +- jsx/components/ContainerPage.tsx | 316 +++++++ jsx/components/ContainerParentForm.tsx | 122 +++ jsx/components/ContainerTab.js | 203 +++++ jsx/components/CustomFields.tsx | 122 +++ jsx/{globals.js => components/Globals.tsx} | 338 ++++--- jsx/{header.tsx => components/Header.tsx} | 125 ++- jsx/components/LifeCycle.tsx | 72 ++ jsx/{listForm.js => components/ListForm.js} | 11 +- .../PoolSpecimenForm.js} | 32 +- jsx/components/PoolTab.js | 182 ++++ jsx/components/ProcessForm.tsx | 251 ++++++ jsx/components/ProcessPanel.tsx | 120 +++ jsx/{search.tsx => components/Search.tsx} | 18 +- .../ShipmentTab.js} | 46 +- jsx/components/SpecimenForm.tsx | 608 +++++++++++++ jsx/components/SpecimenPage.tsx | 182 ++++ jsx/components/SpecimenTab.js | 383 ++++++++ jsx/components/index.ts | 23 + jsx/container.tsx | 195 ---- jsx/containerParentForm.js | 124 --- jsx/containerTab.js | 220 ----- jsx/contexts/BarcodePageContext.tsx | 34 + jsx/contexts/BiobankContext.tsx | 32 + jsx/contexts/index.ts | 2 + jsx/customFields.js | 105 --- jsx/filter.js | 101 --- jsx/helpers.js | 190 ---- jsx/hooks/index.js | 9 + jsx/hooks/useBarcodePageContext.ts | 12 + jsx/hooks/useBiobankContext.ts | 12 + jsx/hooks/useContainer.ts | 111 +++ jsx/hooks/useEditable.ts | 19 + jsx/hooks/useFetch.ts | 69 ++ jsx/hooks/useGenericState.ts | 117 +++ jsx/hooks/useHTTPRequest.ts | 37 + jsx/hooks/useShipment.ts | 53 ++ jsx/hooks/useSpecimen.ts | 68 ++ jsx/hooks/useStreamData.ts | 39 + jsx/i18n/en.json | 4 + jsx/lifeCycle.js | 154 ---- jsx/poolTab.js | 239 ----- jsx/processForm.js | 242 ----- jsx/specimen.tsx | 229 ----- jsx/specimenForm.js | 556 ------------ jsx/specimenTab.js | 411 --------- jsx/types/container.types.ts | 18 + jsx/types/data.types.ts | 8 + jsx/types/handler.types.ts | 22 + jsx/types/index.ts | 9 + jsx/types/label.types.ts | 6 + jsx/types/option.types.ts | 56 ++ jsx/types/pool.types.ts | 16 + jsx/types/shipment.types.ts | 19 + jsx/types/specimen.types.ts | 45 + jsx/utils/clone.ts | 9 + jsx/utils/index.js | 4 + jsx/utils/isEmpty.ts | 21 + jsx/utils/mapFormOptions.ts | 18 + jsx/utils/padBarcode.ts | 24 + jsx/utils/validation.js | 287 ++++++ php/biobank.class.inc | 2 +- php/containercontroller.class.inc | 17 +- php/containerdao.class.inc | 131 ++- ...ndpoint.class.inc => containers.class.inc} | 62 +- php/daoutility.class.inc | 44 + ...belendpoint.class.inc => labels.class.inc} | 2 +- php/module.class.inc | 161 ++-- ...nsendpoint.class.inc => options.class.inc} | 2 +- ...poolendpoint.class.inc => pools.class.inc} | 2 +- php/specimen.class.inc | 51 ++ php/specimencontroller.class.inc | 9 + php/specimendao.class.inc | 42 +- ...endpoint.class.inc => specimens.class.inc} | 20 +- 91 files changed, 5350 insertions(+), 5053 deletions(-) delete mode 100644 jsx/.specimen.js.swp create mode 100644 jsx/APIs/BaseAPI.ts create mode 100644 jsx/APIs/ContainerAPI.ts create mode 100644 jsx/APIs/LabelAPI.ts create mode 100644 jsx/APIs/OptionAPI.ts create mode 100644 jsx/APIs/PoolAPI.ts create mode 100644 jsx/APIs/ShipmentAPI.ts create mode 100644 jsx/APIs/SpecimenAPI.ts create mode 100644 jsx/APIs/index.js delete mode 100644 jsx/Shipment.js delete mode 100644 jsx/barcodePage.js delete mode 100644 jsx/batchProcessForm.js delete mode 100644 jsx/biobankIndex.js rename jsx/{batchEditForm.js => components/BatchEditForm.js} (98%) create mode 100644 jsx/components/BatchProcessForm.tsx create mode 100644 jsx/components/BiobankIndex.js rename jsx/{containerDisplay.js => components/ContainerDisplay.js} (97%) rename jsx/{containerForm.js => components/ContainerForm.js} (66%) create mode 100644 jsx/components/ContainerPage.tsx create mode 100644 jsx/components/ContainerParentForm.tsx create mode 100644 jsx/components/ContainerTab.js create mode 100644 jsx/components/CustomFields.tsx rename jsx/{globals.js => components/Globals.tsx} (56%) rename jsx/{header.tsx => components/Header.tsx} (68%) create mode 100644 jsx/components/LifeCycle.tsx rename jsx/{listForm.js => components/ListForm.js} (98%) rename jsx/{poolSpecimenForm.js => components/PoolSpecimenForm.js} (81%) create mode 100644 jsx/components/PoolTab.js create mode 100644 jsx/components/ProcessForm.tsx create mode 100644 jsx/components/ProcessPanel.tsx rename jsx/{search.tsx => components/Search.tsx} (81%) rename jsx/{shipmentTab.js => components/ShipmentTab.js} (91%) create mode 100644 jsx/components/SpecimenForm.tsx create mode 100644 jsx/components/SpecimenPage.tsx create mode 100644 jsx/components/SpecimenTab.js create mode 100644 jsx/components/index.ts delete mode 100644 jsx/container.tsx delete mode 100644 jsx/containerParentForm.js delete mode 100644 jsx/containerTab.js create mode 100644 jsx/contexts/BarcodePageContext.tsx create mode 100644 jsx/contexts/BiobankContext.tsx create mode 100644 jsx/contexts/index.ts delete mode 100644 jsx/customFields.js delete mode 100644 jsx/filter.js delete mode 100644 jsx/helpers.js create mode 100644 jsx/hooks/index.js create mode 100644 jsx/hooks/useBarcodePageContext.ts create mode 100644 jsx/hooks/useBiobankContext.ts create mode 100644 jsx/hooks/useContainer.ts create mode 100644 jsx/hooks/useEditable.ts create mode 100644 jsx/hooks/useFetch.ts create mode 100644 jsx/hooks/useGenericState.ts create mode 100644 jsx/hooks/useHTTPRequest.ts create mode 100644 jsx/hooks/useShipment.ts create mode 100644 jsx/hooks/useSpecimen.ts create mode 100644 jsx/hooks/useStreamData.ts create mode 100644 jsx/i18n/en.json delete mode 100644 jsx/lifeCycle.js delete mode 100644 jsx/poolTab.js delete mode 100644 jsx/processForm.js delete mode 100644 jsx/specimen.tsx delete mode 100644 jsx/specimenForm.js delete mode 100644 jsx/specimenTab.js create mode 100644 jsx/types/container.types.ts create mode 100644 jsx/types/data.types.ts create mode 100644 jsx/types/handler.types.ts create mode 100644 jsx/types/index.ts create mode 100644 jsx/types/label.types.ts create mode 100644 jsx/types/option.types.ts create mode 100644 jsx/types/pool.types.ts create mode 100644 jsx/types/shipment.types.ts create mode 100644 jsx/types/specimen.types.ts create mode 100644 jsx/utils/clone.ts create mode 100644 jsx/utils/index.js create mode 100644 jsx/utils/isEmpty.ts create mode 100644 jsx/utils/mapFormOptions.ts create mode 100644 jsx/utils/padBarcode.ts create mode 100644 jsx/utils/validation.js rename php/{containerendpoint.class.inc => containers.class.inc} (66%) create mode 100644 php/daoutility.class.inc rename php/{labelendpoint.class.inc => labels.class.inc} (98%) rename php/{optionsendpoint.class.inc => options.class.inc} (99%) rename php/{poolendpoint.class.inc => pools.class.inc} (98%) rename php/{specimenendpoint.class.inc => specimens.class.inc} (84%) diff --git a/jsx/.specimen.js.swp b/jsx/.specimen.js.swp deleted file mode 100644 index 1df49f760ce85b98f03901354dbb2f9d67bee769..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI2dx#xZ9mgkW(svSTTTu}`SxfHjX7Ai28mXJTZIa!DEKRygUIfx4&fYnjoz3jr z$;{l{Y;PB#4+%|8Q^is=7E-F!np9enf>1$=7E51(wEBlu5sFwJNUTLg#P9FSIcH|> zy;-9M6gdMQ?w#}eo!|MLGw1OeTeW+58@t(FZQ*&3WxY7k&(FQF$ojR5nzP$wHfDeElTx(ek z&;>pO7Jzpl$J^i+;C1jKcn+KfkAp|R{h$dvKyvQ^W$;0;3|tMa1XqAxqiyHFGvH}( z2HXek27AFCupSHm27YxF(t~e;GvGA%8fbtB+y=IQHNXbTz^hkU)+^vCa2Sk%o#1kC z?h4C#5}X2G0uO^PfDr5eL*SF({owrNmh~F=J~#`W0FQ!4z~{l|z%W<~7J~0Bwyfje zD7XjQ4Fa$m^n(wBU*h2ANpKR3gO%VN9M-%Ao&{eAkAX+Qm%#(zPH+Hh0ylzpa5(fs z@GN*7d7#Dm0cOT+qeskrMU&X~`23G7xKD+{eQ zbx1u-ym}B<4tvCYL>yZl*tS(UC+#SH9_ z?kuCJS~l0CDIw_q{oCYoc)BFFAf$%IBy1dktjfAcvuP1M>tw?QDy}z8Dbr_w-Zl&k zF%pp!ZYT9!VaTIFC+6KfbFImXxfvUXLnmN0--+Yfohe=|9rS0z2@hg0{jVJQjkr`P zR+`V_xNcciUt;xO(D!PGs!fE-*3PM{ByBz;>gGOEJylL$y}+p@-ZU?zxza#V2DLcV zPgXQ(sfSf>VW=NHltpw)fwba;%aPrPd9=leM|m{m#W75$yCl7i4k$4WG(y)&c&Vp{ zDLY%P2U`3oa&Pax&UaR|?j(Bkrr+IvyFx94@;rr{Tlcki(T%_!F z#6#by@$Sn0J7y|`$um`2U#PPD=IRhg7KPG)^xZPE-8*Q0(` z3hQ2w@TfGGPnC_1t`Hqoj=X~tNtdp3z>$BY22(?&?r*c2)S^-~)r905d{j4@4qa`_ zO)>5-sDj)`WZ7_QtBl>s$i7&AafIY5)MKP`YuhKrtjNyu1P^MSU&=}ov&)6FbMXVx zA(_>9lruoC8AZLN@mlsivS^wZ`ufWvk#^}*(swmMUkSy zSoB_R#bUi|z3l7ec8Kx=lPU7s^ zzO|qFEZm8CE4Je#@`8i57jNf>8(zfSK5Jv@UAjX#mQpK3Vst)69H~}nw(!v$sG!ue z`fPR?kYU0)R3}wSPT=_Hpnj$*A%S2w_&(A6n0{DPTpzW9CoQjo=^}|D)r>{|X>_aK zd=6N9;&=g%L}{{V^j})G$Rn05DmSgI=)W2bRytr>v_oSGBKKAp6K0(nPs~z-mxeYc z&6=m{7lzMO#A6sURL_zLuGL_(;*%dKsx?AY*?`%J>>;$rqr~Ge1_b)wr9ndF>QR#w zEQ-|O#p?d8H!ARwK2)G;EF0A09&0@{@JVg4>_g@Svf8buX<`X_OWq0!==&P0;C4 zz6#4SkgVx+mKIAwl&LkjLep^>BC?`a10;kYCdRS6l3U)ZWX0i#Wz?aoQdMv6%%zY@9=GI@$&f6S?42D6 z&Phc&IyuOq=fwX1THNC_um9BfYaasKt2E)K|fdmevbY9S#SzG3?2e>CU6|w55~X&KxY9x zzyg26xdr(NJOLV@4yJ$)Ccs*-23!NkAK*Oh_kRyw1INK${RE^q^Q0r&ZI*FOmy zuovtGyTB@NJy-<(g1h~nfM>vi;69LmgJ3_{19pJz;41JZ-0wdR?gh7lEnpCA1U7gd z_$%)8-vp0=NpJ^P0e*vf{IlRIU>bN}4D1K{z*a!_{T~5~!RzSDv)}}n1}@kP7@&S$ z0N%#E{>$Jc@I^2J%<~^>faa0(QNvqLOO}e!hwpk)&}j;;5LxSu>8^&zx6@3;*(@WD);c}=JA|*I1 zkjD;Ptrt8TJEw*#pAT2ILgH+*T-+U2o5lBR&z0NFRhzk&LJGaLLxQ$VoizInUa#qW zH+G`J=OZgouHD)kQ!_6UM74RyskL0Tv&~9WD@8OxUc+Z``EwIEg65{ITpH~jT5uJW zD3^?a*2#2ZwkzsI|7`AJljFZ~y=R diff --git a/jsx/APIs/BaseAPI.ts b/jsx/APIs/BaseAPI.ts new file mode 100644 index 00000000..9fbe6c8d --- /dev/null +++ b/jsx/APIs/BaseAPI.ts @@ -0,0 +1,202 @@ +declare const loris: any; + +interface ApiResponse { + data: T, + // Other fields like 'message', 'status', etc., can be added here +} + +interface ApiError { + message: string, + code: number, + // Additional error details can be added here +} + +export interface IAPI { + getAll(): Promise, + getById(id: string): Promise, + create(data: T): Promise, + batchCreate(entities: T[]): Promise, + update(id: string, data: T): Promise, + batchUpdate(entities: T[]): Promise, + // For streamData, you might need a specific return type depending on your implementation + streamData(setProgress: (progress: number) => void): Promise, +} + +function handleHttpErrors(response: Response) { + if (response.status === 404) { + throw new Error("Resource not found"); + } else if (response.status === 401) { + throw new Error("Unauthorized access"); + } else { + throw new Error(`HTTP error! status: ${response.status}`); + } +} + +export class BaseAPI implements IAPI { + protected baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = loris.BaseURL+baseUrl; + } + + async getAll(): Promise { + try { + const response = await fetch(`${this.baseUrl}`); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error("Error in getAll:", error); + throw new Error("An error occurred when fetching data"); + } + } + + async getById(id: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/${id}`); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error(`Error in getById for id ${id}:`, error); + throw new Error(`An error occurred when fetching data for id ${id}`); + } + } + + async create(data: T): Promise { + try { + const response = await fetch(`${this.baseUrl}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error("Error in create:", error); + throw new Error("An error occurred when creating data"); + } + } + + async batchCreate(entities: T[]): Promise { + try { + const response = await fetch(`${this.baseUrl}/batch-create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(entities), + }); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error("Error in batchCreate:", error); + throw new Error("An error occurred when batch creating data"); + } + } + + async update(id: string, data: T): Promise { + try { + const response = await fetch(`${this.baseUrl}/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error(`Error in update for id ${id}:`, error); + throw new Error(`An error occurred when updating data for id ${id}`); + } + } + + async batchUpdate(entities: T[]): Promise { + try { + const response = await fetch(`${this.baseUrl}/batch-update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(entities), + }); + + if (!response.ok) { + handleHttpErrors(response); + } + + return response.json(); + } catch (error) { + console.error("Error in batchUpdate:", error); + throw new Error("An error occurred when batch updating data"); + } + } + + // async delete(id: string): Promise { + // const response: AxiosResponse = await axios.delete(`${this.baseUrl}/${id}`); + // return response.status === 204; + // } + + async streamData(setProgress: (progress: number) => void): Promise { + const response = await fetch(this.baseUrl, { credentials: 'same-origin', method: 'GET' }); + if (!response.body) { + throw new Error('No response body'); + } + + const reader = response.body.getReader(); + const contentLength = +response.headers.get('Content-Length') || 0; + let receivedLength = 0; // received that many bytes at the moment + let chunks = []; // array of received binary chunks (comprises the body) + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + chunks.push(value); + receivedLength += value.length; + + // Update progress + setProgress(Math.round((receivedLength / contentLength) * 100)); + } + + // Combine chunks into single Uint8Array + const chunksAll = new Uint8Array(receivedLength); + let position = 0; + for (let chunk of chunks) { + chunksAll.set(chunk, position); + position += chunk.length; + } + + // Decode into a string + const result = new TextDecoder("utf-8").decode(chunksAll); + + // Parse the result + return JSON.parse(result); + } +} + +export default BaseAPI; diff --git a/jsx/APIs/ContainerAPI.ts b/jsx/APIs/ContainerAPI.ts new file mode 100644 index 00000000..7b4b1f56 --- /dev/null +++ b/jsx/APIs/ContainerAPI.ts @@ -0,0 +1,10 @@ +import BaseAPI from './BaseAPI'; +import { Container } from '../types'; // Assuming you have a User type + +class ContainerAPI extends BaseAPI { + constructor() { + super('/biobank/containers'); // Provide the base URL for container-related API + } +} + +export default ContainerAPI; diff --git a/jsx/APIs/LabelAPI.ts b/jsx/APIs/LabelAPI.ts new file mode 100644 index 00000000..a96ad011 --- /dev/null +++ b/jsx/APIs/LabelAPI.ts @@ -0,0 +1,10 @@ +import BaseAPI from './BaseAPI'; +import { Label } from '../types'; // Assuming you have a User type + +class LabelAPI extends BaseAPI