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; use LIMS::Local::ExcelHandler; # ------------------------------------------------------------------------------ # 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, 95th-percentile or lab test my $self = shift; $self->_debug_path($self->get_current_runmode); my $param = $self->param('id'); # average, 95th-percentile, lab-tests my $vars = $self->query->Vars(); # warn Dumper $vars; my %methods = ( # forwards to selected method: percentile => 'turnaround_percentile', lab_test => 'turnaround_lab_tests', average => 'turnaround_average', ); # check it exists: my $func = $methods{$param} || return $self->error('no method for '.$param); my $first_request_date = $self->model('Request')->get_first_request_date(); $self->tt_params( start_date => $first_request_date ); return $self->forward($func, $vars); } sub turnaround_average : Runmode { # forwarded from turnaround() only my ($self, $vars) = @_; $self->_debug_path($self->get_current_runmode); my ($data, $title) = $self->model('Audit')->turnaround_times_average($vars); # sort data by requested field (defaults to specimen type): my $results = $self->_sort_turnaround_data_average($data); # AoH (sorted) return $self->tt_process({ data => $results, title => $title }); } sub turnaround_percentile : Runmode { # forwarded from turnaround() only my ($self, $vars) = @_; $self->_debug_path($self->get_current_runmode); my ($data, $title) = $self->model('Audit')->turnaround_times_percentile($vars); my $results = $self->_calc_turnaround_data_percentile($data); return $self->tt_process({ data => $results, title => $title }); } sub turnaround_lab_tests : Runmode { # forwarded from turnaround() only my ($self, $vars) = @_; $self->_debug_path($self->get_current_runmode); # set default duration 90d, 1yr too slow: if (! $vars->{constraint_type}) { @{$vars}{qw/constraint_type days/} = qw(days 90); } my ($data, $title) = $self->model('Audit')->turnaround_times_lab_tests($vars); return $self->tt_process({ data => $data, title => $title }); } # ------------------------------------------------------------------------------ sub turnaround_chart : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); my $specimen = $self->param('id'); my %vars = $self->query->Vars(); # hash to avoid affecting self_url: $vars{specimen} = $specimen; # warn Dumper $vars; my ($data, $title) = $self->model('Audit')->turnaround_times_chart(\%vars); # _dump_data($data); # to dump for manual calculation eg Excel 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; # sort unique elements into (natural) numeric order (for use as bins): my @monotonic = LIMS::Local::Utils::n_sort($unique); # warn Dumper \@monotonic; # generate frequency distribution data using monotonic 'bins': my $f = $stat->frequency_distribution_ref(\@monotonic); # warn Dumper $f; $s{chart_data} = $f; # stash for session } # callback for .tt (scalar context = value, list context = value + index): my $x_percentile = sub { my $x = $stat->percentile(@_); return $x }; { my %stats = ( min => $stat->min(), max => $stat->max(), sum => $stat->sum(), mean => $stat->mean(), mode => $stat->mode(), range => $stat->sample_range(), total => $stat->count(), median => $stat->median(), marker => &$x_percentile(95), # for chart to draw 95% marker std_dev => $stat->standard_deviation(), variance => $stat->variance(), skewness => $stat->skewness(), trimmed_mean => $stat->trimmed_mean(0,0.05), # or (0.05) for 5% either side ); # warn Dumper \%stats; $s{chart_stats} = \%stats; # stash for session $self->tt_params( 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(), specimen_type => $specimen, x_percentile => $x_percentile, ); return $self->tt_process(); } # ------------------------------------------------------------------------------ sub turnaround_data : 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 $xl = LIMS::Local::ExcelHandler->new; my @headers = qw(request_number year sample_code screened registered authorised delta); my $data = $self->model('Audit')->turnaround_times_data(\%vars); # arrayref my $output = $xl->generate_spreadsheet(\@headers, $data); $specimen =~ s/\W+/_/g; # replace non-word chars with underscore $specimen =~ s/_\Z//g; # remove trailing underscore $self->header_props(-type => 'application/excel', -expires => 'now', -attachment => lc $specimen . '.xls'); return $output; } # ------------------------------------------------------------------------------ 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 edit_revised_diagnosis : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); my $request_id = $self->param('id'); my $request = $self->model('Request')->get_single_request($request_id); my $data = $self->model('Audit')->edit_revised_diagnosis($request_id); my $opts = $self->model('Base')->get_objects('DiagnosisChangeOption'); $self->tt_params( data => $data, opts => $opts, request => $request, ); return $self->tt_process(); } # ------------------------------------------------------------------------------ sub do_edit_diagnosis_revision : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); my $id = $self->param('id'); # request_diagnosis_history.id my $vars = $self->query->Vars(); $vars->{id} = $id; my $url = $self->query->url() . '/audit/revised_diagnoses?sort_by=' . $vars->{sort_by}; unless ( $vars->{_option_id} != $vars->{option_id} ) { $self->flash(warning => $self->messages('request_edit')->{edit_failed}); return $self->redirect( $url ); } my $rtn = $self->model('Request')->update_request_diagnosis_history($vars); if ($rtn) { return $self->error($rtn); } else { $self->flash( info => $self->messages('action')->{edit_success} ); return $self->redirect( $url ); } } # ------------------------------------------------------------------------------ sub user_workload : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); 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(); } # ------------------------------------------------------------------------------ =begin # to get duration to lab-test status, but only recorded in history file: sub lab_tests_to_status : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); my $vars = $self->query->Vars(); # warn Dumper $vars; { # get lab sections status options map: my $status_options_map = $self->all_lab_section_status_options_map(); $self->tt_params( status_options => $status_options_map ); } return $self->tt_process(); } =cut # ------------------------------------------------------------------------------ sub lab_tests_sign_off : Runmode { my $self = shift; $self->_debug_path($self->get_current_runmode); my $vars = $self->query->Vars(); # warn Dumper $vars; # set default duration 90d (has no effect unless lab_section_id supplied): if (! $vars->{constraint_type}) { @{$vars}{qw/constraint_type days/} = qw(days 90); } # lab_section_id passed either as menu param, or modify constraints as id: my $lab_section_id = $self->param('id') || $vars->{lab_section_id}; { # get lab sections map - needed irrespective of lab_section_id param: my $lab_sections_map = $self->lab_sections_map(); $self->tt_params( lab_sections_map => $lab_sections_map ); } if ( $lab_section_id ) { $vars->{lab_section_id} ||= $lab_section_id; # in case it's a param('id') my ($data, $title) = $self->model('Audit')->lab_tests_sign_off($vars); my %h; for (@$data) { my $test = $_->{field_label}; # array to retain sort order (f): push @{ $h{$test} }, { user => $_->{username}, f => $_->{f} }; }; $self->tt_params( data => \%h, title => $title, section_id => $lab_section_id, # start_date => $self->model('Request')->get_first_request_date(), # doesn't work - WTF?? ); } return $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; } sub _dump_data { my $data = shift; open my $fh, '>', './data.txt'; print $fh $_ . ",\n" for @$data; } 1;