RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Admin::Patient::Merge;

use base 'LIMS::Base';

use Moose;
with (
	'LIMS::Controller::Roles::SearchConstraint', # generate_search_constraints()
	'LIMS::Controller::Roles::SessionStore',
);

__PACKAGE__->meta->make_immutable(inline_constructor => 0);

use LIMS::Local::Utils;

use Data::Dumper;

#-------------------------------------------------------------------------------
sub default : StartRunmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	my $errs = shift; # $self->stash( errs => $errs );
	
	return $self->forbidden() unless $self->user_can('patient_merge');

    $self->js_validation_profile('patient_merge');

	return $self->tt_process($errs);
}

#-------------------------------------------------------------------------------
sub search : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	
	return $self->forbidden() unless $self->user_can('patient_merge');

    my $dfv = $self->check_rm( 'default', $self->validate('patient_merge') )
    || return $self->dfv_error_page;

    my $data = $dfv->valid; # $self->debug($data);	

	unless ( $self->get_current_runmode eq 'do_merge' ) { # ie re-direct
		# store params in session to save multiple hidden fields:
		my $vars = $self->query->Vars(); # need scalar context to get hashref
		$self->session_store_form_data($vars);
	}
	
	# protect against submission of empty form:
    my $search_constraints = $self->generate_search_constraints($data) 
    || return $self->forward('default'); # $self->debug($search_constraints);
	
	my $records = $self->_get_records_for_merge($search_constraints);
	$self->tt_params( records => $records ); # warn Dumper $records;
	
	my $count = scalar @$records; # warn $count;
	if ( $count > 50 ) {
		my $msg = sprintf $self->messages('patient')->{merge}->{too_many}, $count;
		$self->flash( warning => $msg );
		# return $self->forward('default');	# probably not - would prevent merge
		# on patient with high number of follow-ups - so just set flash msg
	}
	
	if ($count) { # warn Dumper $_->as_tree for @$records;
		# get list of unique patients sorted according to sort_by input:
		my $patients = $self->_sort_patients($records);		
		$self->tt_params( patients => $patients );
	}
	else {
		# just set flash warning:
		$self->flash( info => $self->messages('search')->{no_records_found} );		
	}

	return $self->tt_process('admin/patient/merge/default.tt');
}

#-------------------------------------------------------------------------------
# returns array(ref) of hashrefs of flattened RDBO::Manager objects
sub _get_records_for_merge {
	my ($self, $search_constraints) = @_;
	
	# RDBO::Manager object containing patient_case arrays of request arrays:
	my $o = $self->model('Patient')->get_records_for_merge($search_constraints);
	my $cloner = sub { LIMS::Local::Utils::clone(@_) };
	my $delta  = sub { LIMS::Local::Utils::delta_business_days(@_) };

	my $today = LIMS::Local::Utils::today->ymd('-');

	# get array of hashes (patient data, location, unit_no, labno, diagnosis, reported)
	my @records;
	REF: for my $ref (@$o) { # warn Dumper $ref; # ref contains arrays of arrays
		my %h = (); # reset
		$h{patient}{$_} = $ref->$_ for
			qw(id last_name first_name middle_name nhs_number gender dob); # warn Dumper \%h;

		# get patient_case (an array of PatientCase objects):
		my $patient_case = $ref->patient_case; # warn Dumper $patient_case; # AoH
		CASE: for my $case (@$patient_case) { # warn ref $case; # hashref
			my $c = &$cloner(\%h); # $self->debug($c); # hashref
			$c->{patient}{patient_case} = $case->as_tree;

			# get request data or add patient data:
			my $request = $case->{request} || []; # must exists as arrayref
			if ( not @$request ) { push @records, $c; next CASE }

			for my $req (@$request) { # warn Dumper $req;
				my $d = &$cloner($c); # $self->debug($d); # hashref
				$d->{request} = $req;
				{ # calculate age of report in delta business days:
					my $report_date = $req->created_at->ymd('-') || $today; # deflate
					$d->{request}{delta_report} = &$delta($report_date, $today);
				} # warn Dumper $d;
				push @records, $d;
			}
		}
	} # warn Dumper \@records;
	return \@records;
}

#-------------------------------------------------------------------------------
sub do_merge : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	
	return $self->forbidden() unless $self->user_can('patient_merge');
	
	my $params = $self->query->Vars(); # $self->debug($params);	
	{ # need to put original search params back into query for forward to search():
		my $search_params = $self->session->param('_patient_merge_data');
		while ( my ($param, $val) = each %$search_params ) {
			$self->query->param($param, $val);
		}
	}
	
	# initialise data structure to hold 'to' & 'from' data:
	my $data = {
		from => [], # can be array of 1 or more patient_case_id's
		to   => [], # must ONLY have array size of 1 
	};
	
	PARAM: # sort form params into %$data hash:
	while ( my($patient_case_id, $direction_of_change) = each %$params ) {
		# skip irrelevant form params:
		next PARAM unless grep $direction_of_change eq $_, qw(from to);
		
		my $list = $data->{$direction_of_change}; # ie 'to' or 'from' key
		push @$list, $patient_case_id;
	} # $self->debug($data);

	# check we have AT LEAST one 'from' & ONLY one 'to':
	unless ( scalar (@{ $data->{from} }) >= 1 && scalar (@{ $data->{to} }) == 1 ) {
		$self->flash( warning => $self->messages('patient')->{merge}->{not_flagged} );
		return $self->search();	# don't use forward() here - need to keep current rm
	}
	
	# require confirmation:
	unless ( $self->query->param('confirm_merge') ) {
		return $self->_generate_merge_confirmation($data);
	}
	
	# add scope param to $data:
	$data->{scope} = $self->query->param('scope');

	my $rtn = $self->model('Patient')->merge_patients($data);
	
	if ($rtn) {
		return $self->error($rtn);
	}
	else {
		$self->flash( info => $self->messages('patient')->{merge}->{merge_success} );
		return $self->search();	# don't use forward() here - need to keep current rm
	}
}

#-------------------------------------------------------------------------------
sub _generate_merge_confirmation {
	my $self = shift; $self->_debug_path();
	my $data = shift;
	
	{ # from:
		my @ids = @{ $data->{from} };	
		my $cases = $self->model('PatientCase')->get_patient_cases(\@ids);
		$self->tt_params( merge_from => $cases );
	}
	{ # to:
		my ($id) = @{ $data->{to} };
		my $case = $self->model('PatientCase')->get_patient_case($id);	
	
		$self->tt_params( merge_to => $case );
	}
	
	return $self->tt_process('admin/patient/merge/confirm.tt');
}

#-------------------------------------------------------------------------------
# get list of unique patients sorted according to sort_by input:
# possible sorts: id, name, dob, unit_no, nhs_no, count:
sub _sort_patients {
	my ($self, $records) = @_; $self->_debug_path();
	
	# extract unique patients from $records arrayref:
	my %unique_patients; # as hash to eliminate duplicates
	for (@$records) { # hashref of patient, location and request hashrefs:
		my $patient = $_->{patient}; # warn Dumper $patient;
		my $patient_id = $patient->{id};
		
		$unique_patients{$patient_id}{count}++; # auto increment
		# convert patient_case object to hashref for clarity downstream:		
		$unique_patients{$patient_id}{patient} ||= $patient;
	}; # warn Dumper \%unique_patients;

	# sort order supplied by user, or default to patient_id:
	my $sort_by = $self->query->param('sort_by') || 'patient_id'; # warn $sort_by;

	# dispatch table of sort methods:
	my %methods = (
		dob	        => \&_sort_by_dob,
		name        => \&_sort_by_name,
		count       => \&_sort_by_count,
		nhs_number  => \&_sort_by_nhs_number,
		patient_id  => \&_sort_by_patient_id,
		unit_number => \&_sort_by_unit_number,		
	);
	
	# get required sort_method:
	my $sort_method = $methods{$sort_by};
	
	# pass \%unique_patients to the required sort method & get sorted list of
	# patient.id's back:
	my $patient_ids = $sort_method->(\%unique_patients);
	
	# create an array of patient data hashrefs, sorted according to @ids:
	my @sorted_patients = map $unique_patients{$_}, @$patient_ids;
	return \@sorted_patients;
}

sub _sort_by_dob {
	my $unique_patients = shift;
	
	my $NULL = DateTime->now; # need a datetime object for null dob's
	
	my @ids = sort {
		my $A = $unique_patients->{$a}{patient}{dob} || $NULL; 
		my $B = $unique_patients->{$b}{patient}{dob} || $NULL;
			
		DateTime->compare( $A, $B );			
	} keys %$unique_patients; 

	return \@ids;
}

sub _sort_by_name {
	my $unique_patients = shift;

	my @ids = sort {
		my $A_LN = $unique_patients->{$a}{patient}{last_name};
		my $B_LN = $unique_patients->{$b}{patient}{last_name};
			
		my $A_FN = $unique_patients->{$a}{patient}{first_name};
		my $B_FN = $unique_patients->{$b}{patient}{first_name};
			
		$A_LN cmp $B_LN || $A_FN cmp $B_FN;
	} keys %$unique_patients; 

	return \@ids;
}

sub _sort_by_unit_number {
	my $unique_patients = shift;

	my @ids = sort {
		$unique_patients->{$a}{patient}{unit_number}
			cmp
		$unique_patients->{$b}{patient}{unit_number}
	} keys %$unique_patients;
	
	return \@ids;
}
	
sub _sort_by_nhs_number {			
	my $unique_patients = shift; # warn Dumper $unique_patients;

	my @ids = sort {
		my $A = $unique_patients->{$a}{patient}{nhs_number} || 0;
		my $B = $unique_patients->{$b}{patient}{nhs_number} || 0;

		$A <=> $B;
	} keys %$unique_patients;
	
	return \@ids;
}

sub _sort_by_count {
	my $unique_patients = shift;

	my @ids = sort {
		$unique_patients->{$a}{count} <=> $unique_patients->{$b}{count};
	} keys %$unique_patients;
	
	return \@ids;
}

sub _sort_by_patient_id { # default if nothing supplied
	my $unique_patients = shift; 
	
	return [ sort { $a <=> $b } keys %$unique_patients ];
} 

1;