GemsTracker/gemstracker-library

View on GitHub
classes/Gems/Agenda/Appointment.php

Summary

Maintainability
F
6 days
Test Coverage
D
60%
<?php

/**
 *
 * @package    Gems
 * @subpackage Agenda
 * @author     Matijs de Jong <mjong@magnafacta.nl>
 * @copyright  Copyright (c) 2014 Erasmus MC
 * @license    New BSD License
 */

use Gems\Agenda\AppointmentFilterInterface;
use Gems\Agenda\EpisodeOfCare;
use Gems\Agenda\FilterTracer;

/**
 *
 * @package    Gems
 * @subpackage Agenda
 * @copyright  Copyright (c) 2014 Erasmus MC
 * @license    New BSD License
 * @since      Class available since version 1.6.3
 */
class Gems_Agenda_Appointment extends \MUtil_Translate_TranslateableAbstract
{
    /**
     *
     * @var int The id of the appointment
     */
    protected $_appointmentId;

    /**
     *
     * @var array The gems appointment data
     */
    protected $_gemsData = array();

    /**
     *
     * @var \Gems_Agenda
     */
    protected $agenda;

    /**
     *
     * @var \Gems_User_User
     */
    protected $currentUser;

    /**
     *
     * @var \Zend_Db_Adapter_Abstract
     */
    protected $db;

    /**
     * True when the token does exist.
     *
     * @var boolean
     */
    public $exists = true;

    /**
     *
     * @var \Gems\Agenda\FilterTracer
     */
    protected $filterTracer;

    /**
     *
     * @var \Gems_Loader
     */
    protected $loader;

    /**
     * Creates the appointments object
     *
     * @param mixed $appointmentData Appointment Id or array containing appointment record
     */
    public function __construct($appointmentData)
    {
        if (is_array($appointmentData)) {
            $this->_gemsData      = $appointmentData;
            $this->_appointmentId = $appointmentData['gap_id_appointment'];
            if ($this->currentUser instanceof \Gems_User_User) {
                $this->_gemsData = $this->currentUser->applyGroupMask($this->_gemsData);
            }
        } else {
            $this->_appointmentId = $appointmentData;
            // loading occurs in checkRegistryRequestAnswers
        }
    }

    /**
     * Create a new track for this appointment and the given filter
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker $tracker
     */
    protected function _createTrack($filter, $tracker)
    {
        $trackData = array('gr2t_comment' => sprintf(
                        $this->_('Track created by %s filter'),
                        $filter->getName()
                        ));

        $fields    = array($filter->getFieldId() => $this->getId());
        $trackId   = $filter->getTrackId();
        $respTrack = $tracker->createRespondentTrack(
                $this->getRespondentId(),
                $this->getOrganizationId(),
                $trackId,
                $this->currentUser->getUserId(),
                $trackData,
                $fields
                );

        return $respTrack;
    }

    /**
     * Makes sure the respondent data is part of the $this->_gemsData
     */
    protected function _ensureRespondentOrgData()
    {
        if (! isset($this->_gemsData['gr2o_id_user'], $this->_gemsData['gco_code'])) {
            $sql = "SELECT *
                FROM gems__respondents INNER JOIN
                    gems__respondent2org ON grs_id_user = gr2o_id_user INNER JOIN
                    gems__consents ON gr2o_consent = gco_description
                WHERE gr2o_id_user = ? AND gr2o_id_organization = ? LIMIT 1";

            $respId = $this->_gemsData['gap_id_user'];
            $orgId  = $this->_gemsData['gap_id_organization'];
            // \MUtil_Echo::track($this->_gemsData);

            if ($row = $this->db->fetchRow($sql, array($respId, $orgId))) {
                $this->_gemsData = $this->_gemsData + $row;
            } else {
                $appId = $this->_appointmentId;
                throw new \Gems_Exception("Respondent data missing for appointment id $appId.");
            }
        }
    }

    /**
     * Check if a track should be created for any of the filters
     *
     * @param \Gems\Agenda\AppointmentFilterInterface[] $filters
     * @param array $existingTracks Of $trackId => [RespondentTrack objects]
     * @param \Gems_Tracker $tracker
     *
     * @return int Number of tokenchanges
     */
    protected function checkCreateTracks($filters, $existingTracks, $tracker)
    {
        $tokenChanges = 0;

        // Check for tracks that should be created
        foreach ($filters as $filter) {
            if (!$filter->isCreator()) {
                continue;
            }

            $createTrack = true;

            // Find the method to use for this creator type
            $method      = $this->getCreatorCheckMethod($filter->getCreatorType());
            $trackId     = $filter->getTrackId();
            $tracks      = array_key_exists($trackId, $existingTracks) ? $existingTracks[$trackId] : [];

            foreach($tracks as $respTrack) {
                /* @var $respTrack \Gems_Tracker_RespondentTrack */
                if (!$respTrack->hasSuccesCode()) { continue; }

                $createTrack = $this->$method($filter, $respTrack);
                if ($createTrack === false) {
                    break;  // Stop checking
                }
            }
            if ($this->filterTracer) {
                $this->filterTracer->addFilter($filter, $createTrack, $respTrack);

                if (! $this->filterTracer->executeChanges) {
                    $createTrack = false;
                }
            }

            // \MUtil_Echo::track($trackId, $createTrack, $filter->getName(), $filter->getSqlAppointmentsWhere(), $filter->getFilterId());
            if ($createTrack) {
                $respTrack = $this->_createTrack($filter, $tracker);
                $existingTracks[$trackId][] = $respTrack;

                $tokenChanges += $respTrack->getCount();
                if ($this->filterTracer) {
                    $this->filterTracer->addFilter($filter, $createTrack, $respTrack);
                }
            }
        }

        return $tokenChanges;
    }

    /**
     * Should be called after answering the request to allow the Target
     * to check if all required registry values have been set correctly.
     *
     * @return boolean False if required are missing.
     */
    public function checkRegistryRequestsAnswers()
    {
        if ($this->db && (! $this->_gemsData)) {
            $this->refresh();
        }

        return $this->exists;
    }

    /**
     * Has the track ended <wait days> ago?
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createAfterWaitDays($filter, $respTrack)
    {
        $createTrack = true;
        $curr        = $this->getAdmissionTime();
        $end         = $respTrack->getEndDate();
        $wait        = $filter->getWaitDays();

        if ((! $end) || ($curr->diffDays($end) <= $wait)) {
            $createTrack = false;
            if ($this->filterTracer) {
                if (! $end) {
                    $this->filterTracer->setSkipCreationMessage(
                            $this->_('track without an end date')
                            );
                } else {
                    $this->filterTracer->setSkipCreationMessage(sprintf(
                            $this->_('%d days since previous end date, %d required'),
                            $curr->diffDays($end)                            ,
                            $wait
                            ));
                }
            }
        }
        if ($createTrack) {
            // Test to see whether this track has already been created by this filter
            $fieldId = $filter->getFieldId();
            $data    = $respTrack->getFieldData();
            if (isset($data[$fieldId]) && ($data[$fieldId] == $this->_appointmentId)) {
                $createTrack = false;
                if ($this->filterTracer) {
                    $this->filterTracer->setSkipCreationMessage(
                        $this->_('track has already been created')
                    );
                }
            }
        }

        return $createTrack;
    }

    /**
     * Always report the track should be created
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createAlways($filter, $respTrack)
    {
        $createTrack = $this->createAfterWaitDays($filter, $respTrack);

        if ($createTrack) {
            $createTrack = $this->createWhenNotInThisTrack($filter, $respTrack);
        }

        return $createTrack;
    }

    /**
     * Always report the track should be created
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createAlwaysNoEndDate($filter, $respTrack)
    {
        return $this->createWhenNotInThisTrack($filter, $respTrack);
    }

    /**
     * Always report the track should be created
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     * @return boolean
     */
    public function createFromStart($filter, $respTrack)
    {
        $createTrack = true;
        $curr        = $this->getAdmissionTime();
        $start       = $respTrack->getStartDate();
        $wait        = $filter->getWaitDays();

        if ((! $start) || ($curr->diffDays($start) <= $wait)) {
            $createTrack = false;
            if ($this->filterTracer) {
                if (! $start) {
                    $this->filterTracer->setSkipCreationMessage(
                            $this->_('track without a startdate')
                            );
                } else {
                    $this->filterTracer->setSkipCreationMessage(sprintf(
                            $this->_('%d days since previous startdate, %d required'),
                            $curr->diffDays($start)                            ,
                            $wait
                            ));
                }
            }
        }
        if ($createTrack) {
            // Test to see whether this track has already been created by this filter
            $fieldId = $filter->getFieldId();
            $data    = $respTrack->getFieldData();
            if (isset($data[$fieldId]) && ($data[$fieldId] == $this->_appointmentId)) {
                $createTrack = false;
                if ($this->filterTracer) {
                    $this->filterTracer->setSkipCreationMessage(
                        $this->_('track has already been created')
                    );
                }
            }
        }

        return $createTrack;
    }

    /**
     * Always return the track should NOT be created
     *
     * This should never be called as 0 is not a creator, the code is here just
     * to make sure calling without checking has the correct result
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createNever()
    {
        if ($this->filterTracer) {
            $this->filterTracer->setSkipCreationMessage($this->_('never create a track'));
        }
        return false;
    }

    /**
     * Only return true when no open track exists
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createNoOpen($filter, $respTrack)
    {
        // If an open track of this type exists: do not create a new one
        $createTrack = !$respTrack->isOpen();

        if ($createTrack) {
            $createTrack = $this->createWhenNotInThisTrack($filter, $respTrack);
        } elseif ($this->filterTracer) {
            $this->filterTracer->setSkipCreationMessage(
                    $this->_('an open track exists')
                    );
        }

        return $createTrack;
    }

    /**
     * Create when current appointment is not assigned to this field already
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createWhenNotInThisTrack($filter, $respTrack)
    {
        $createTrack = true;

        $data = $respTrack->getFieldData();
        if (isset($data[$filter->getFieldId()]) &&
                ($this->getId() == $data[$filter->getFieldId()])) {
            $createTrack = false;

            if ($this->filterTracer) {
                $this->filterTracer->setSkipCreationMessage(
                        $this->_('appointment used in track')
                        );
            }
        }

        return $createTrack;
    }

    /**
     * Only return true when no open track exists
     *
     * @param \Gems\Agenda\AppointmentFilterInterface $filter
     * @param \Gems_Tracker_RespondentTrack $respTrack
     *
     * @return boolean
     */
    public function createWhenNoOpen($filter, $respTrack)
    {
        // If an open track of this type exists: do not create a new one
        $createTrack = !$respTrack->isOpen();

        if ($createTrack) {
            $createTrack = $this->createAfterWaitDays($filter, $respTrack);
        } elseif ($this->filterTracer) {
            $this->filterTracer->setSkipCreationMessage(
                    $this->_('an open track exists')
                    );
        }

        if ($createTrack) {
            $createTrack = $this->createWhenNotInThisTrack($filter, $respTrack);
        }

        return $createTrack;
    }

    /**
     * Return the description of the current ativity
     *
     * @return string or null when not found
     */
    public function getActivityDescription()
    {
        if (! (isset($this->_gemsData['gap_id_activity']) && $this->_gemsData['gap_id_activity'])) {
            $this->_gemsData['gaa_name'] = null;
        }
        if (!array_key_exists('gaa_name', $this->_gemsData)) {
            $sql = "SELECT gaa_name FROM gems__agenda_activities WHERE gaa_id_activity = ?";

            $this->_gemsData['gaa_name'] = $this->db->fetchOne($sql, $this->_gemsData['gap_id_activity']);

            // Cleanup db result
            if (false === $this->_gemsData['gaa_name']) {
                $this->_gemsData['gaa_name'] = null;
            }
        }
        return $this->_gemsData['gaa_name'];
    }

    /**
     * Return the id of the current activity
     *
     * @return int
     */
    public function getActivityId()
    {
        return $this->_gemsData['gap_id_activity'];
    }

    /**
     * Return the admission time
     *
     * @return \MUtil_Date Admission time as a date or null
     */
    public function getAdmissionTime()
    {
        if (isset($this->_gemsData['gap_admission_time']) && $this->_gemsData['gap_admission_time']) {
            if (! $this->_gemsData['gap_admission_time'] instanceof \MUtil_Date) {
                $this->_gemsData['gap_admission_time'] =
                        new \MUtil_Date($this->_gemsData['gap_admission_time'], \Gems_Tracker::DB_DATETIME_FORMAT);
            }
            // Clone to make sure calculations can be performed without changing this object
            return clone $this->_gemsData['gap_admission_time'];
        }
    }

    /**
     * Get the DB id of the attending by person
     *
     * @return int
     */
    public function getAttendedById()
    {
        return $this->_gemsData['gap_id_attended_by'];
    }

    /**
     * The cooment to the appointment
     *
     * @return string
     */
    public function getComment()
    {
        return $this->_gemsData['gap_comment'];
    }

    /**
     * Get method to call to check track creation
     *
     * @param int $type
     * @return string The method to call in this class
     */
    public function getCreatorCheckMethod($type)
    {
        static $methods = [
            0 => 'createNever',
            1 => 'createWhenNoOpen',
            2 => 'createAlways',
            3 => 'createAlwaysNoEndDate',
            4 => 'createFromStart',
            5 => 'createNoOpen',
        ];

        // No checks, when type does not exists this is an error we want to be thrown
        return $methods[$type];
    }

    /**
     * Get a general description of this appointment
     *
     * @see \Gems_Agenda->getAppointmentDisplay()
     *
     * @return string
     */
    public function getDisplayString()
    {
        $results[] = $this->getAdmissionTime()->toString($this->agenda->appointmentDisplayFormat);
        $results[] = $this->getActivityDescription();
        $results[] = $this->getProcedureDescription();
        $results[] = $this->getLocationDescription();
        $results[] = $this->getSubject();

        return implode($this->_('; '), array_filter($results));
    }

    /**
     *
     * @return \Gems|Agenda\EpisodeOfCare
     */
    public function getEpisode()
    {
        $episodeId = $this->getEpisodeId();

        if ($episodeId) {
            return $this->agenda->getEpisodeOfCare($episodeId);
        }
    }

    /**
     *
     * @return int
     */
    public function getEpisodeId()
    {
        return $this->_gemsData['gap_id_episode'];
    }

    /**
     * Return the appointment id
     *
     * @return int
     */
    public function getId()
    {
        return $this->_appointmentId;
    }

    /**
     * Return the description of the current location
     *
     * @return string or null when not found
     */
    public function getLocationDescription()
    {
        if (! (isset($this->_gemsData['gap_id_location']) && $this->_gemsData['gap_id_location'])) {
            $this->_gemsData['glo_name'] = null;
        }
        if (!array_key_exists('glo_name', $this->_gemsData)) {
            $sql = "SELECT glo_name FROM gems__locations WHERE glo_id_location = ?";

            $this->_gemsData['glo_name'] = $this->db->fetchOne($sql, $this->_gemsData['gap_id_location']);

            // Cleanup db result
            if (false === $this->_gemsData['glo_name']) {
                $this->_gemsData['glo_name'] = null;
            }
        }
        return $this->_gemsData['glo_name'];
    }

    /**
     * Get the DB id of the location
     *
     * @return int
     */
    public function getLocationId()
    {
        return $this->_gemsData['gap_id_location'];
    }

    /**
     *
     * @return int
     */
    public function getOrganizationId()
    {
        return $this->_gemsData['gap_id_organization'];
    }

    /**
     *
     * @return string The respondents patient number
     */
    public function getPatientNumber()
    {
        if (! isset($this->_gemsData['gr2o_patient_nr'])) {
            $this->_ensureRespondentOrgData();
        }

        return $this->_gemsData['gr2o_patient_nr'];
    }

    /**
     * Return the description of the current procedure
     *
     * @return string or null when not found
     */
    public function getProcedureDescription()
    {
        if (! (isset($this->_gemsData['gap_id_procedure']) && $this->_gemsData['gap_id_procedure'])) {
            return null;
        }
        if (!array_key_exists('gapr_name', $this->_gemsData)) {
            $sql = "SELECT gapr_name FROM gems__agenda_procedures WHERE gapr_id_procedure = ?";

            $this->_gemsData['gapr_name'] = $this->db->fetchOne($sql, $this->_gemsData['gap_id_procedure']);

            // Cleanup db result
            if (false === $this->_gemsData['gapr_name']) {
                $this->_gemsData['gapr_name'] = null;
            }
        }
        return $this->_gemsData['gapr_name'];
    }

    /**
     * Return the id of the current procedure
     *
     * @return int
     */
    public function getProcedureId()
    {
        return $this->_gemsData['gap_id_procedure'];
    }

    /**
     * Get the DB id of the referred by person
     *
     * @return int
     */
    public function getReferredById()
    {
        return $this->_gemsData['gap_id_referred_by'];
    }

    /**
     * Return the respondent object
     *
     * @return \Gems_Tracker_Respondent
     */
    public function getRespondent()
    {
        return $this->loader->getRespondent(
                $this->getPatientNumber(),
                $this->getOrganizationId(),
                $this->getRespondentId())
            ;
    }

    /**
     * Return the user / respondent id
     *
     * @return int
     */
    public function getRespondentId()
    {
        return $this->_gemsData['gap_id_user'];
    }

    /**
     * The source of the appointment
     *
     * @return string
     */
    public function getSource()
    {
        return $this->_gemsData['gap_source'];
    }

    /**
     * The source id of the appointment
     *
     * @return string
     */
    public function getSourceId()
    {
        return $this->_gemsData['gap_id_in_source'];
    }

    /**
     * The subject of the appointment
     *
     * @return string
     */
    public function getSubject()
    {
        return isset($this->_gemsData['gap_subject']) ? $this->_gemsData['gap_subject'] : null;
    }

    /**
     *
     * @return boolean
     */
    public function hasEpisode()
    {
        return (boolean) $this->_gemsData['gap_id_episode'];
    }

    /**
     * Return true when the status is active
     *
     * @return type
     */
    public function isActive()
    {
        return $this->exists &&
                isset($this->_gemsData['gap_status']) &&
                $this->agenda->isStatusActive($this->_gemsData['gap_status']);
    }

    /**
     *
     * @param array $gemsData Optional, the data refresh with, otherwise refresh from database.
     * @return \Gems_Agenda_Appointment (continuation pattern)
     */
    public function refresh(array $gemsData = null)
    {
        if (is_array($gemsData)) {
            $this->_gemsData = $gemsData + $this->_gemsData;
        } else {
            $select = $this->db->select();
            $select->from('gems__appointments')
                    ->where('gap_id_appointment = ?', $this->_appointmentId);

            $this->_gemsData = $this->db->fetchRow($select);
            if (false == $this->_gemsData) {
                // on failure, reset to empty array
                $this->_gemsData = array();
            }
        }
        $this->exists = isset($this->_gemsData['gap_id_appointment']);

        if ($this->currentUser instanceof \Gems_User_User) {
            $this->_gemsData = $this->currentUser->applyGroupMask($this->_gemsData);
        }

        return $this;
    }

    /**
     *
     * @param AppointmentFilterTracer $tracer
     * @return $this
     */
    public function setFilterTracer(FilterTracer $tracer)
    {
        $this->filterTracer = $tracer;

        return $this;
    }

    /**
     * Recalculate all tracks that use this appointment
     *
     * @return int The number of tokens changed by this code
     */
    public function updateTracks()
    {
        $tokenChanges = 0;
        $tracker      = $this->loader->getTracker();

        // Find all the fields that use this agenda item
        $select = $this->db->select();
        $select->from('gems__respondent2track2appointment', array('gr2t2a_id_respondent_track'))
                ->joinInner(
                        'gems__respondent2track',
                        'gr2t_id_respondent_track = gr2t2a_id_respondent_track',
                        array('gr2t_id_track')
                        )
                ->where('gr2t2a_id_appointment = ?', $this->_appointmentId)
                ->distinct()
                ->order('gr2t_id_track');

        // AND find the filters for any new fields to fill
        $filters = $this->agenda->matchFilters($this);
        if ($filters) {
            $ids = array_map(function ($value) {
                return $value->getTrackId();
            }, $filters);

            // \MUtil_Echo::track(array_keys($filters), $ids);
            $respId = $this->getRespondentId();
            $orgId  = $this->getOrganizationId();
            $select->orWhere(
                    "gr2t_id_user = $respId AND gr2t_id_organization = $orgId AND gr2t_id_track IN (" .
                    implode(', ', $ids) . ")"
                    );
            // \MUtil_Echo::track($this->getId(), implode(', ', $ids));
        }

        // \MUtil_Echo::track($select->__toString());

        // Now find all the existing tracks that should be checked
        $respTracks = $this->db->fetchPairs($select);

        // \MUtil_Echo::track($respTracks);
        $existingTracks = array();
        if ($respTracks) {
            foreach ($respTracks as $respTrackId => $trackId) {
                $respTrack = $tracker->getRespondentTrack($respTrackId);

                // Recalculate this track
                $fieldsChanged = false;
                if ((! $this->filterTracer) || $this->filterTracer->executeChanges) {
                    $changed = $respTrack->recalculateFields($fieldsChanged);
                } else {
                    $changed = 0;
                }
                if ($this->filterTracer) {
                    $this->filterTracer->addTrackChecked($respTrack, $fieldsChanged, $changed);
                }
                $tokenChanges += $changed;

                // Store the track for creation checking
                $existingTracks[$trackId][] = $respTrack;
            }
        }

        // Only check if we need to create when this appointment is active and today or later
        if ($this->isActive() && $this->getAdmissionTime()->isLaterOrEqual(new \MUtil_Date())) {
            $tokenChanges += $this->checkCreateTracks($filters, $existingTracks, $tracker);
        } else {
            if ($this->filterTracer) {
                $this->filterTracer->setSkippedFilterCheck();
            }
        }

        return $tokenChanges;
    }
}