package LIMS::Controller::Audit;
use Moose;
BEGIN { extends 'LIMS::Base'; }
with (
'LIMS::Controller::Roles::Misc',
'LIMS::Controller::Roles::SessionStore',
);
use namespace::clean -except => 'meta';
__PACKAGE__->meta->make_immutable(inline_constructor => 0);
use Data::Dumper;
use Statistics::Descriptive;
# ------------------------------------------------------------------------------
# default() should never be called direct - redirect to start page:
sub default : StartRunmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
return $self->redirect( $self->query->url );
}
# ------------------------------------------------------------------------------
sub turnaround : Runmode { # calculates as average and 95th-percentile
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $selection = $self->param('id'); # eg specimen_type, location, etc
my $calc_method = $self->param('Id'); # average, 95th-percentile
my $vars = $self->query->Vars();
$vars->{selection} = $selection; # warn Dumper $vars;
my ($data, $title, $parsed, $tt);
if ( $calc_method eq 'average' ) {
($data, $title)
= $self->model('Audit')->turnaround_times_average($vars);
# sort data by requested field (defaults to specimen type):
$parsed = $self->_sort_turnaround_data_average($data); # AoH (sorted)
$tt = 'audit/turnaround_average.tt';
}
elsif ( $calc_method eq 'percentile' ) {
($data, $title)
= $self->model('Audit')->turnaround_times_percentile($vars);
$parsed = $self->_calc_turnaround_data_percentile($data);
$tt = 'audit/turnaround_percentile.tt';
}
$self->tt_params(
data => $parsed,
title => $title,
start_date => $self->model('Request')->get_first_request_date(),
);
return $self->tt_process($tt);
}
# ------------------------------------------------------------------------------
sub turnaround_chart : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $specimen = $self->param('id');
my $vars = $self->query->Vars();
$vars->{specimen} = $specimen; # warn Dumper $vars;
my ($data, $title) = $self->model('Audit')->turnaround_times_chart($vars); # warn Dumper $data;
# warn Dumper [ LIMS::Local::Utils::n_sort($data) ]; # to dump for manual calculation
my $stat = Statistics::Descriptive::Full->new();
$stat->add_data(@$data);
my %s = ( chart_title => $title ); # for session
{ # generate frequency data & add to session for chart:
my $unique = LIMS::Local::Utils::get_unique_elements($data); # warn Dumper $unique;
my @monotonic = LIMS::Local::Utils::n_sort($unique); # warn Dumper \@monotonic;
my %f = $stat->frequency_distribution(\@monotonic); # warn Dumper \%f;
# require every x-axis val from 0 to max to get 95th percentile line consistent:
my $max_val = LIMS::Local::Utils::get_max_val(\@monotonic);
$f{$_} ||= 0 for ( 0 .. $max_val ); # so chart doesn't skip empties
$s{chart_data} = \%f;
}
# callback for .tt (call in scalar context to get value only):
my $x_percentile = sub { my $x = $stat->percentile(@_); $x };
$self->tt_params( x_percentile => $x_percentile );
my $ninety_five = $stat->percentile(95); # call in scalar context
my %stats = (
count => $stat->count(),
mean => $stat->mean(),
sum => $stat->sum(),
median => $stat->median(),
mode => $stat->mode(),
range => $stat->sample_range(),
marker => $ninety_five,
std_dev => $stat->standard_deviation(),
variance => $stat->variance(),
);
$s{chart_stats} = \%stats;
# add all _turnaround_chart_* to session:
$self->session->param( _turnaround_chart => \%s );
$self->tt_params(
start_date => $self->model('Request')->get_first_request_date(),
stats => \%stats,
);
return $self->tt_process();
}
# ------------------------------------------------------------------------------
sub nhs_number : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $vars = $self->query->Vars();
my ($data, $title) = $self->model('Audit')->nhs_number_supplied($vars);
$self->tt_params(
data => $data,
title => $title,
start_date => $self->model('Request')->get_first_request_date(),
);
return $self->tt_process();
}
# ------------------------------------------------------------------------------
sub revised_diagnoses : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $vars = $self->query->Vars(); # warn Dumper $vars;
$vars->{sort_by} ||= 'date'; # temp. until constraints can pass env vars
my ($data, $title) = $self->model('Audit')->revised_diagnoses($vars);
my $total_reported = $data->{total_reported}; # count
my $user_reported = $data->{user_report_counts}; # if sort_by ne 'date'
my $user_details = $data->{user_details}; # if sort_by ne 'date'
my $revisions = $data->{revisions}; # arrayref; maybe manipulated below, so:
my $request_ids = $data->{request_ids}; # arrayref
my $rev_count = scalar @$request_ids; # revisions count
# get specimens map:
my $specimen_map = $self->specimen_map($request_ids);
$self->tt_params(
total_count => $total_reported,
specimen_map => $specimen_map,
user_details => $user_details,
user_counts => $user_reported,
rev_count => $rev_count,
revisions => $revisions,
title => $title,
sort_by => $vars->{sort_by},
start_date => $self->model('Request')->get_first_request_date(),
);
return $self->tt_process();
}
# ------------------------------------------------------------------------------
sub user_workload : Runmode {
my $self = shift;
my $vars = $self->query->Vars(); # warn Dumper $vars;
my @actions = qw( screened reported authorised );
my %args = ( action => \@actions, vars => $vars );
my $data = $self->model('Audit')->user_workload(\%args); # warn Dumper $data;
my $stats = $data->{stats}; # AoH
$self->stash( duration => $data->{title}, vars => $vars ); # for .tt
my %h1 = (); # for chart
my %h2 = (); # for table cols
my %h3 = (); # for individual result breakdown
for my $entry (@$stats) { # warn Dumper $entry;
my $request_specimen = $entry->{specimen};
my $specimen_type = $self->_get_specimen_type($request_specimen);
# warn Dumper [$request_specimen, $specimen_type];
my $action = $entry->{action};
my $user = $entry->{username};
$h1{$action}{$user}++;
$h2{$action}{$user}{$specimen_type}++;
$h3{$action}{$user}{$specimen_type}{$request_specimen}++;
} # warn Dumper [\%h1, \%h2, \%h3];
# freeze stats data in session for chart (to save 3 more db queries):
$self->session_store_hashref(\%h1, '_user_workload_data');
$self->tt_params(
data => \%h2,
detail => \%h3,
sum => sub { LIMS::Local::Utils::sum_list(@_) }, # callback to calculate totals
);
$self->tt_process();
}
# ------------------------------------------------------------------------------
sub _get_specimen_type {
my ($self, $sample_code) = @_; # warn $sample_code if $sample_code =~ '-';
# only interested in BMAT, BMA, PB or Tissue biopsy:
return $sample_code if grep $sample_code eq $_, qw(BMAT BMA PB); # single specimens
return 'Tissue' if $sample_code =~ /\A[DGLRTX](A|[BS][LP]|F|U)/;
# for multi-specimen requests:
return 'BMAT' if $sample_code =~ /BMAT/;
return 'BMA' if $sample_code =~ /BMA/;
return 'PB' if $sample_code =~ /PB/;
return 'Other'; # everything else, including multi-specimen request not matched
}
# ------------------------------------------------------------------------------
sub _calc_turnaround_data_percentile {
my $self = shift;
my $data = shift;
my %h;
while ( my($specimen, $d) = each %$data ) { # warn Dumper $d;
for my $action ( qw/delta_report delta_authorise/ ) {
next unless $d->{$action}; # next unless $specimen eq 'foo';
# next unless $specimen eq 'Slide';
=begin # using Statistics::Descriptive
# sort numerical vals in ascending order:
my @vals = sort { $a <=> $b } @{ $d->{$action} }; # warn Dumper \@vals;
# take array position of 95%:
my $index = @vals * 0.95; warn $index; # array pos for 95% value
# get value for 95%:
my $x = $vals[ int ( $index - 1 ) ]; # warn Dumper [$specimen, $x];
=cut
my $stat = Statistics::Descriptive::Full->new();
$stat->add_data( @{ $d->{$action} } );
my ($x, $index) = $stat->percentile(95); # warn Dumper [$index, $x];
$h{$specimen}{$action} = $x;
}
$h{$specimen}{count} = $d->{count};
}
return \%h;
}
# ------------------------------------------------------------------------------
sub _sort_turnaround_data_average {
my $self = shift;
my $data = shift;
# default order_by is specimen type:
my $order_by = $self->query->param('order_by') || 'type';
my %methods = (
type => \&_sort_by_specimen_type,
totals => \&_sort_by_totals,
delta_r2a => \&_sort_by_delta_r2a, # report -> auth diff
delta_auth => \&_sort_by_delta_authorise,
delta_report => \&_sort_by_delta_report,
);
# get required sort method:
my $sort_method = $methods{$order_by};
# get sort order for $data as a list of $data keys:
my $sort_order = $sort_method->($data); # warn Dumper $sort_order;
# add $data key (specimen type) to its own hashref val:
$data->{$_}->{type} = $_ for keys %$data;
# create array from $data in requested sort order:
my @sorted_data = map $data->{$_}, @$sort_order; # add $data vals in sort order
return \@sorted_data;
}
# ------------------------------------------------------------------------------
sub _sort_by_specimen_type {
my $data = shift; # warn Dumper $data;
my @sort_order = sort { $a cmp $b } keys %$data;
return \@sort_order;
}
# ------------------------------------------------------------------------------
sub _sort_by_totals {
my $data = shift; # warn Dumper $data; # hashref
my @sort_order = sort {
$data->{$a}->{count} <=> $data->{$b}->{count}
|| # 2nd sort by keys (ie specimen type) in case similar counts:
$a cmp $b
} keys %$data; # warn Dumper \@sort_order;
return \@sort_order;
}
# ------------------------------------------------------------------------------
sub _sort_by_delta_report {
my $data = shift; # warn Dumper $data;
my @sort_order = sort {
$data->{$a}->{delta_report} / $data->{$a}->{count}
<=>
$data->{$b}->{delta_report} / $data->{$b}->{count}
|| $a cmp $b # 2nd sort by keys (ie specimen type) in case similar counts
} keys %$data;
return \@sort_order;
}
# ------------------------------------------------------------------------------
sub _sort_by_delta_authorise {
my $data = shift; # warn Dumper $data;
my @sort_order = sort {
$data->{$a}->{delta_authorise} / $data->{$a}->{count}
<=>
$data->{$b}->{delta_authorise} / $data->{$b}->{count}
|| $a cmp $b # 2nd sort by keys (ie specimen type) in case similar counts
} keys %$data;
return \@sort_order;
}
# ------------------------------------------------------------------------------
sub _sort_by_delta_r2a { # diff. between delta_report & delta_authorisation
my $data = shift; # warn Dumper $data;
my @sort_order = sort {
(
$data->{$a}->{delta_authorise} / $data->{$a}->{count} -
$data->{$a}->{delta_report} / $data->{$a}->{count}
)
<=>
(
$data->{$b}->{delta_authorise} / $data->{$b}->{count} -
$data->{$b}->{delta_report} / $data->{$b}->{count}
)
|| $a cmp $b # 2nd sort by keys (ie specimen type) in case similar counts
} keys %$data;
return \@sort_order;
}
1;