RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Storage;

=begin # FluidX XTR-96 plate reader:
requires xtr_96_addr ip:port in settings file to provide access

System settings:
    Winsock: Add <CRLF> = OFF; Enable Winsock = ON; Port = 2001
    Barcode: Integral reader (ensure Matrix ID auto orientate off)

Results settings:
    Header information: Single Field; Label: Rack Identifier (checked)
    Results Format: Tube IDs and TubeRack Coords
    Spacing of results: commas only
    Separation of records: same line
    Grouping of records: by Column
    Results capture: Require RackID with tube readings
=cut

use LIMS::ControllerClass; # inherits LIMS::Base and provides LIMS::Local::Debug::p
with 'LIMS::Controller::Roles::RecordHandler';
__PACKAGE__->meta->make_immutable(inline_constructor => 0);

use Net::Telnet;
use Data::Dumper;

# ------------------------------------------------------------------------------
# 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 load : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift; $self->stash( errs => $errs );
    
    my $request_id = $self->param('id')
    || return $self->error('no id passed to ' . $self->get_current_runmode);

	# get request data:
    my $request_data = $self->model('Request')->get_single_request($request_id);
	# get existing storage data:
	my $storage = $self->model('Storage')->get_request_storage($request_id);
    # vials in storage NOT signed out:
    my @available = map $_->vialId, grep ! $_->signed_out, @$storage;
	# get specimen map for request:
    my $specimen_map = $self->specimen_map([ $request_id ]); # warn Dumper $specimen_map;

    my %tt_params = (
        request_data => $request_data,
        specimen_map => $specimen_map,
        available    => \@available,
		storage      => $storage,        
    );
    
	{ # menu options:
        my $opts = $self->_get_menu_options();
        $tt_params{menu_options} = $opts;
    }
    # yaml config to allow new tests to be requested (eg xNA extraction):
    if ( my $yml = $self->get_yaml_file('storage') ) { # p $yml;
        # only allowed 1 lab section as method for result_update expects section id:        
        my ($section) = keys %$yml; # p $section;
        
        my $o = $self->model('LabTest')->get_lab_tests; # warn Dumper $_ for @$o;
        
        { # lab tests:
            my %h = map +($_->field_label => $_->id),
                grep $_->lab_section->section_name eq $section, @$o; # p %h;
            # take hash slice:
            my $lab_tests = $yml->{$section}; # p $lab_tests;
            my %tests = map +($_ => $h{$_}), @$lab_tests; # p %tests;
            $tt_params{lab_tests} = \%tests;
        }
        { # lab section details:
            my %h =
                map +($_->lab_section->section_name => $_->lab_section_id), @$o;
            my %lab_section = ( name => $section, id => $h{$section} ); # p %lab_section;
            $tt_params{lab_section} = \%lab_section;
        }        
        { # request-lab-test data for $section:
            my %h = ( section_name => $section, request_id => $request_id );
            my $request_lab_test =
                $self->model('LabTest')->get_request_lab_tests_for_section(\%h);
            my %map = map +($_->lab_test->field_label => 1), @$request_lab_test;
            $tt_params{request_lab_tests} = \%map;
        }
        { # record status (RecordHandler::is_record_complete):
            my $is_locked = $self->is_record_complete($request_data); # p $is_locked;
            $tt_params{is_locked} = $is_locked; # will be undef if status != complete
        }
    }

    $self->tt_params(%tt_params);
	return $self->render_view($self->tt_template_name, $errs);    
}

# ------------------------------------------------------------------------------
sub input : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift;
    
    my $request_id = $self->param('id')
    || return $self->error('no id passed to ' . $self->get_current_runmode);

    my $dfv = $self->check_rm('load', $self->validate('storage_input') )
	|| return $self->dfv_error_page;
    
    my $params = $dfv->valid; # print p $params;
    $params->{request_id} = $request_id;

	my $rtn = $self->model('Storage')->input_storage($params);
	return $self->error( $rtn ) if $rtn;
	
    # insert flash message
	$self->flash( info => $self->messages('action')->{create_success} );
    return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}

# ------------------------------------------------------------------------------
sub output : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift;
    
    my $request_id = $self->param('id')
    || return $self->error('no id passed to ' . $self->get_current_runmode);

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

    my $params = $dfv->valid; # warn Dumper $params;    
    $params->{request_id} = $request_id;
    
    my $rtn = $self->model('Storage')->output_storage($params);
    
    if (ref $rtn eq 'HASH') { # will be error
        return $self->forward('load', $rtn);
    }
    
    # insert flash message:
    $self->flash( info => $self->messages('action')->{edit_success} );
    return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}

# ------------------------------------------------------------------------------
sub edit : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift; $self->stash( errs => $errs );
    
    my $vial_id = $self->param('id')
    || return $self->error('no id passed to ' . $self->get_current_runmode);
    
	{ # menu options:
        my $opts = $self->_get_menu_options();
        $self->tt_params( menu_options => $opts );
    }
	my $data = $self->model('Storage')->get_storage_vial($vial_id);
    $self->tt_params( data => $data );

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

# ------------------------------------------------------------------------------
sub update_storage_vial : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift;
    
    my $vial_id = $self->param('id')
    || return $self->error('no vial_id passed to ' . $self->get_current_runmode);
    
    my $request_id = $self->param('Id')
    || return $self->error('no request_id passed to ' . $self->get_current_runmode);
    
    my $dfv = $self->check_rm('edit', $self->validate('storage_input') )
	|| return $self->dfv_error_page;

    my $params = $dfv->valid; # p $params;
    $params->{request_id} = $request_id;
    $params->{vial_id}    = $vial_id;

	my $rtn = $self->model('Storage')->update_storage($params);
	return $self->error( $rtn ) if $rtn;
	
    # insert flash message
	$self->flash( info => $self->messages('action')->{edit_success} );
    return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}

# ------------------------------------------------------------------------------
sub read_xtr_96 : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	
	my $cfg = $self->cfg('settings'); # p $cfg->{xtr_96_addr};

	# connection info, or return:
	my ($host, $port) = split ':', $cfg->{xtr_96_addr}; # p [$host, $port];
	return $self->error('require host & port') unless ($host && $port);

	# request to commence plate scan:
	if ( $self->query->param('scan') ) {
		# get data or return (any failure will be in flash msg):
		my $xtr_96 = $self->_get_xtr_96_data($host, $port) # hashref of plate_data & plateId
		|| return $self->redirect( $self->query->url . '/storage/read_xtr_96' );
		
		{ # get storage_rack & request_storage db table data (if exists):
			my $plate_data = $xtr_96->{plate_data};
			my $plateId    = $xtr_96->{plateId};

			my $storage_rack = $self->model('Storage')->get_storage_rack($plateId); # hashref
            my @vial_ids     = values %{ $plate_data }; # p(@vial_ids); p($storage_rack);

			my %args = $storage_rack # if exists, use rack id, otherwise vial_id's:
				? ( rack_id  => $storage_rack->{id} ) # get all vials from rack
				: ( vial_ids => \@vial_ids ); # get all vials matching input
				
			my $o = $self->model('Storage')->get_rack_contents(\%args);
			# need hash of vialId's from request_storage where signed_out is null:
			my %rack_data = map { $_->vialId => 1 } grep { ! $_->signed_out } @$o;
				
			# do we have any vials in rack which do NOT exist in storage:
			my $have_missing = grep { ! $rack_data{$_} } @vial_ids;
	
			my %h = (
				storage_rack => $storage_rack,
				have_missing => $have_missing,
				rack_data    => \%rack_data,
			); # p %h;
			$xtr_96->{storage} = \%h;
		}
		
		$self->tt_params( data => $xtr_96 );

		# store in session for retrieval after 'import':
		$self->session->param( _xtr_96_data => $xtr_96 );
	}
    else { # ensure only PC attached to xtr_96 can activate scan:
        my ($ip, $port) = split ':', $self->cfg('settings')->{xtr_96_addr};
        my $ok = $ENV{DEVEL_SERVER} || ( $ENV{REMOTE_ADDR} eq $ip );
        $self->tt_params(
            remote_addr => $ENV{REMOTE_ADDR},
            xtr_96_addr => $ip,
            can_scan    => $ok,
        );
    }
	return $self->tt_process();
}

# ------------------------------------------------------------------------------
sub import_xtr_96_data : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	
	my $xtr_96_data = $self->session->dataref->{_xtr_96_data}
	|| return $self->error( 'no xtr-96 data retrieved from session' ); # p($xtr_96_data);

	# get storage_rack data (if exists):
	my $storage_data = $xtr_96_data->{storage}; # p($storage_data);

    # check no missing data (shouldn't happen - should not have been able to submit):
    if ( $storage_data->{have_missing} ) {
        my $url = $self->query->url . '/storage/read_xtr_96?scan=1';
        return $self->redirect( $url );
    }
    
    # if storage_rack exists, check is_active:
    if ( my $data = $storage_data->{storage_rack} ) {
        # probably going to sign out all vials in rack:
    }
    else { # new import:
        my $plate_id = $xtr_96_data->{plateId} # eg SA00098711
        || return $self->error( 'no plate id retrieved from session' ); # p($plate_id);

        # insert new entry in storage_racks, returns storage_racks.id:
        my $rack_id = $self->model('Storage')->new_storage_rack($plate_id);
        
        my @vialIds = keys %{ $storage_data->{rack_data} }; # p(@vialIds);
        
        my %args = (
            rack_id => $rack_id,
            vialId  => \@vialIds,
        ); # p(%args);
        my $rows = $self->model('Storage')->update_rack_id(\%args); # p($rows);

        # number of $rows should equal @vialIds:
        if ( $rows == @vialIds ) {
            return $self->tt_process({ data => \@vialIds });
        }
        else { # some vialId's not updated:
            
        }
    }
	return $self->dump_html; # move to own sub - get request info from vial posn
}

# ------------------------------------------------------------------------------
sub _get_xtr_96_data {
	my $self = shift; $self->_debug_path();
	my ($host, $port) = @_;
	
#	return _dummy_data(); # uncomment to use test data

	my @args = (
		Timeout	=> 15, 
		Errmode	=> 'return', # don't die (default)
#		Input_Log 	=> './xtr-96.log', # uncomment to debug	
	);		
	my $telnet = Net::Telnet->new(@args);

	my @host = ( Host => $host, Port => $port );
	unless ( $telnet->open(@host) ) { # returns 1 on success, 0 on failure:
		$self->flash( error => $telnet->errmsg ); # failure msg in errmsg()
		return 0;
	}
	$telnet->cmd('get');

	# direct manipulation of buffer easier than documented methods!!
	my $ref = $telnet->buffer; # p $ref;
	$telnet->close;

	my ($header, $body) = split "\n", ${$ref}; # p [$header, $body];

	my ($plateId) = $header =~ /Rack Identifier = ([A-Z0-9]+)/;
#	my %plate_data = ( $str =~ /^([A-H][0-9]{2})\,\s?(.+?)$/mg ); # if $str is list format
	my %data = split /,\s?/, $body; # p %data;
	
	my %h = (
		plate_data => \%data,
		plateId	   => $plateId,
	); # p %h;
	
	return \%h;
}

sub _get_menu_options {
    my $self = shift;
    my $yaml = $self->get_yaml_file('menu_options'); # p $yaml;
    my $opts = $yaml->{storage_options};
    return $opts;
}

1;

#===============================================================================
sub _dummy_data { # dummy data to spare plate reader ...
	my $plateId = 'SA00098711';

	my %data = (
          'D04' => 'FC12803113',
          'H06' => 'FC12803280',
          'F06' => 'FC12803202',
          'F04' => 'FC12801012',
          'B01' => 'FC12800770',
          'C11' => 'FC12803515',
          'E06' => 'FC12800872',
          'G03' => 'FC12803499',
          'F07' => 'FC12803399',
          'F01' => 'FC12803170',
          'B03' => 'FC12803391',
          'B02' => 'FC12800850',
          'B07' => 'FC12800740',
          'D05' => 'FC12801010',
          'F05' => 'FC12803423',
          'A02' => 'FC12803415',
          'D02' => 'FC12801029',
          'B05' => 'FC12801091',
          'G04' => 'FC12800842',
          'G01' => 'FC12800965',
          'H02' => 'FC12803407',
          'D03' => 'FC12803487',
          'B06' => 'FC12803151',
          'C06' => 'FC12803135',
          'H03' => 'FC12800929',
          'D01' => 'FC12803208',
          'C01' => 'FC12800680',
          'B10' => 'FC12803459',
          'F09' => 'FC12803372',
          'H04' => 'FC12801106',
          'G11' => 'FC12803243',
          'C12' => 'FC12800934',
          'A05' => 'FC12803203',
          'E08' => 'FC12800953',
          'C03' => 'FC12803288',
          'C05' => 'FC12803099',
          'A09' => 'FC12800671',
          'C07' => 'FC12803405',
          'F08' => 'FC12803511',
          'B11' => 'FC12800993',
          'H12' => 'FC12800871',
          'A01' => 'FC12800704',
          'D11' => 'FC12801040',
          'A10' => 'FC12801037',
          'D10' => 'FC12803246',
          'H09' => 'FC12803219',
          'A06' => 'FC12801061',
          'D09' => 'FC12803385',
          'H05' => 'FC12800678',
          'A11' => 'FC12803392',
          'H11' => 'FC12800771',
          'E07' => 'FC12803480',
          'B08' => 'FC12803131',
          'D08' => 'FC12803072',
          'E11' => 'FC12803140',
          'G02' => 'FC12800958',
          'H01' => 'FC12800972',
          'A04' => 'FC12800686',
          'A07' => 'FC12800705',
          'A08' => 'FC12803350',
          'C10' => 'FC12800648',
          'G06' => 'FC12801079',
          'C09' => 'FC12801027',
          'F03' => 'FC12800737',
          'F10' => 'FC12803476',
          'G09' => 'FC12800873',
          'F02' => 'FC12803365',
          'E03' => 'FC12801120',
          'B04' => 'FC12803458',
          'E12' => 'FC12800948',
          'G08' => 'FC12803293',
          'H08' => 'FC12803401',
          'C04' => 'FC12800955',
          'H07' => 'FC12803196',
          'E01' => 'No Tube',
          'G05' => 'FC12803207',
          'E05' => 'FC12800774',
          'B09' => 'FC12800987',
          'C02' => 'FC12803386',
          'D06' => 'FC12800753',
          'D07' => 'FC12800724',
          'E04' => 'FC12800857',
          'E02' => 'FC12803185',
          'E10' => 'FC12800805',
          'H10' => 'FC12801055',
          'G12' => 'FC12803439',
          'B12' => 'FC12800957',
          'E09' => 'FC12803403',
          'A03' => 'FC12803358',
          'D12' => 'FC12803394',
          'F11' => 'FC12803502',
          'F12' => 'No Read',
          'A12' => 'FC12800868',
          'G07' => 'FC12800693',
          'G10' => 'FC12800964',
          'C08' => 'FC12800777',
	); # warn Dumper \%data;	
	
	return { # same format as xtr-96 output:
		plate_data => \%data,
		plateId	   => $plateId,
	}
}