Files
hpt_inventory/resources/views/box_positions.blade.php
2026-08-17 10:18:11 +07:00

611 lines
39 KiB
PHP

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@include('layout.header')
<style>
.hpis-page-title {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 22px;
}
.hpis-title-icon {
width: 58px;
height: 58px;
border-radius: 50%;
background: #eaf2ff;
color: #1263e8;
display: grid;
place-items: center;
font-size: 30px;
}
.hpis-title-text h3 {
margin: 0;
color: #061735;
font-size: 25px;
font-weight: 800;
}
.hpis-title-text p {
margin: 3px 0 0;
color: #52627a;
font-size: 13px;
}
.hpis-card {
background: #fff;
border: 1px solid #e1e7ef;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, .03);
}
.hpis-section-title {
display: flex;
align-items: center;
gap: 10px;
color: #071731;
font-weight: 800;
font-size: 15px;
margin-bottom: 14px;
}
.hpis-step {
width: 22px;
height: 22px;
border-radius: 50%;
background: #1263e8;
color: #fff;
display: inline-grid;
place-items: center;
font-size: 12px;
font-weight: 800;
}
.hpis-field-row {
display: grid;
grid-template-columns: 126px 1fr;
align-items: center;
gap: 10px;
margin-bottom: 8px;
font-size: 13px;
}
.hpis-field-label {
color: #0f1f3d;
font-weight: 600;
}
.hpis-fake-input {
min-height: 32px;
border: 1px solid #dce3ec;
border-radius: 5px;
background: #fff;
padding: 7px 10px;
color: #0f1f3d;
line-height: 1.2;
}
.hpis-fake-input.muted {
background: #f1f3f6;
color: #667085;
}
.position-grid {
display: grid;
grid-template-columns: repeat({{$box->columns ?: 10}}, 39px);
gap: 12px;
justify-content: center;
}
.box-map-shell {
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 16px 18px;
background: #fff;
}
.box-map-header {
display: grid;
grid-template-columns: 30px repeat({{$box->columns ?: 10}}, 39px);
gap: 12px;
margin-bottom: 8px;
justify-content: center;
}
.box-map-body {
display: grid;
grid-template-columns: 30px 1fr;
gap: 12px;
}
.grid-label {
height: 22px;
display: flex;
align-items: center;
justify-content: center;
color: #344054;
font-size: 12px;
font-weight: 600;
}
.row-labels {
display: grid;
grid-template-rows: repeat({{$box->rows ?: 1}}, 39px);
gap: 12px;
}
.position-card {
border: 1px solid #d8dbe0;
border-radius: 6px;
width: 39px;
height: 39px;
min-height: 39px;
padding: 3px;
cursor: pointer;
background: #f8fff9;
overflow: hidden;
}
.position-card.occupied {
background: #ebf9f0;
border-color: #52b788;
}
.position-card.reserved {
background: #fff8e6;
border-color: #f6ad2f;
background-image: repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(246, 173, 47, .25) 3px, rgba(246, 173, 47, .25) 6px);
}
.position-card.quarantine {
background: #f4ebff;
border-color: #9b5de5;
}
.position-card.selected {
border: 2px solid #0d6efd;
box-shadow: 0 0 0 3px rgba(13, 110, 253, .12);
}
.position-code {
font-weight: 700;
font-size: 10px;
}
.aliquot-number {
font-size: 7.5px;
word-break: break-word;
line-height: 1.15;
margin-top: 3px;
max-height: 24px;
overflow: hidden;
}
.position-meta {
font-size: 9px;
color: #667085;
line-height: 1.1;
}
.position-card .badge {
font-size: 0;
width: 7px;
height: 7px;
padding: 0;
border-radius: 50%;
}
.selected-panel {
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 16px;
background: #fff;
min-height: 100%;
}
.selected-position-title {
color: #0d6efd;
font-size: 28px;
font-weight: 700;
}
.selected-detail-label {
color: #667085;
font-size: 12px;
margin-top: 12px;
}
.selected-detail-value {
color: #101828;
font-size: 13px;
font-weight: 500;
}
.legend-dot {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 4px;
border: 1px solid #d8dbe0;
vertical-align: middle;
margin-right: 5px;
}
.hpis-audit table {
font-size: 12px;
}
</style>
</head>
<body>
<div class="container-scroller">
@include('layout.navbar')
@include('layout.breadscrum')
<div class="container-fluid page-body-wrapper">
@include('layout.sidebar')
<div class="main-panel">
<div class="content-wrapper">
<div class="hpis-page-title">
<div class="hpis-title-icon"><i class="mdi mdi-cube-outline"></i></div>
<div class="hpis-title-text">
<h3>Register Freezer Box</h3>
<p>Register and manage a freezer box to store sample tubes in the inventory system.</p>
</div>
</div>
<div class="row">
<div class="col-xl-3 col-lg-4 mb-3">
<div class="hpis-card p-3 mb-2">
<div class="hpis-section-title"><span class="hpis-step">1</span> Box Information</div>
<div class="hpis-field-row"><div class="hpis-field-label">Box ID <span class="text-danger">*</span></div><div class="hpis-fake-input">{{$box->code}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Box Name <span class="text-danger">*</span></div><div class="hpis-fake-input">{{$box->name}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Barcode</div><div class="hpis-fake-input">{{$box->code ? 'BCX'.$box->code : '-'}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Box Type <span class="text-danger">*</span></div><div class="hpis-fake-input">{{$box->capacity}} Position</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Position Format</div><div class="hpis-fake-input muted">{{$box->rows}} x {{$box->columns}} ({{$box->capacity}})</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Material</div><div class="hpis-fake-input">Cryobox</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Color</div><div class="hpis-fake-input"><span class="legend-dot" style="background:#1263e8;border-color:#1263e8;"></span> Blue</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Status</div><div class="hpis-fake-input">Active</div></div>
</div>
<div class="hpis-card p-3 mb-2">
<div class="hpis-section-title"><span class="hpis-step">2</span> Storage Location</div>
<div class="hpis-field-row"><div class="hpis-field-label">Facility <span class="text-danger">*</span></div><div class="hpis-fake-input">{{session('base_organization_name') ?? 'National Biobank'}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Building <span class="text-danger">*</span></div><div class="hpis-fake-input">{{optional(optional(optional(optional($box->rack)->equipment)->room)->building)->name}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Room <span class="text-danger">*</span></div><div class="hpis-fake-input">{{optional(optional($box->rack)->equipment)->room ? optional(optional($box->rack)->equipment)->room->name : '-'}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Freezer <span class="text-danger">*</span></div><div class="hpis-fake-input">{{optional($box->rack)->equipment ? optional($box->rack->equipment)->name : '-'}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Rack <span class="text-danger">*</span></div><div class="hpis-fake-input">{{optional($box->rack)->name}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Position <span class="text-danger">*</span></div><div class="hpis-fake-input" id="left_selected_position">-</div></div>
</div>
<div class="hpis-card p-3">
<div class="hpis-section-title"><span class="hpis-step">3</span> Additional Properties</div>
<div class="hpis-field-row"><div class="hpis-field-label">Owner Lab</div><div class="hpis-fake-input">Virology Unit</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Notes</div><div class="hpis-fake-input muted">Enter notes (optional)...</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Created By</div><div class="hpis-fake-input muted">{{optional(Auth::user())->name ?? 'ratha.leng'}}</div></div>
<div class="hpis-field-row"><div class="hpis-field-label">Created Date</div><div class="hpis-fake-input muted">{{$box->created_at}}</div></div>
</div>
</div>
<div class="col-xl-7 col-lg-8 mb-3">
<div class="hpis-card p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="hpis-section-title mb-0"><span class="hpis-step">4</span> Box Layout <span class="text-muted">({{$box->capacity}} Position: {{$box->rows}} x {{$box->columns}})</span></div>
<div>
<button class="btn btn-sm btn-outline-primary mr-2" type="button">Box Type Guide</button>
<button class="btn btn-sm btn-outline-secondary" id="btnGeneratePositions" data-box_id="{{$box->id}}" type="button"><i class="mdi mdi-cog-outline">Generate Positions</i></button>
</div>
</div>
@if($positions->count() == 0)
<div class="alert alert-warning">
No positions have been generated for this box yet. Click <strong>Generate Positions</strong> to create A1, A2, B1... slots from the box rows and columns.
</div>
@else
<div class="box-map-shell">
<div class="box-map-header">
<div></div>
@for($column = 1; $column <= $box->columns; $column++)
<div class="grid-label">{{$column}}</div>
@endfor
</div>
<div class="box-map-body">
<div class="row-labels">
@for($row = 1; $row <= $box->rows; $row++)
<div class="grid-label">{{chr(64 + $row)}}</div>
@endfor
</div>
<div class="position-grid">
@foreach($positions as $position)
@php($aliquot = $position->aliquot)
@php($statusClass = $aliquot ? strtolower($aliquot->status ?? 'occupied') : '')
<div class="position-card {{$aliquot ? 'occupied' : ''}} {{$statusClass}}"
data-position_id="{{$position->id}}"
data-position_code="{{$position->code}}"
data-aliquot_id="{{optional($aliquot)->id}}"
data-aliquot_number="{{optional($aliquot)->aliquot_number}}"
data-status="{{optional($aliquot)->status ?: ($aliquot ? 'Occupied' : 'Empty')}}"
data-remaining_volume="{{optional($aliquot)->remaining_volume}}"
data-volume_unit="{{optional($aliquot)->volume_unit}}"
data-sample="{{optional(optional($aliquot)->sample)->sample_code}}"
data-pathogen="{{optional(optional(optional($aliquot)->sample)->pathogen)->scientific_name}}">
<div class="d-flex justify-content-between">
<span class="position-code">{{$position->code}}</span>
<span class="badge {{$aliquot ? 'badge-success' : 'badge-light'}}">{{$aliquot ? ($aliquot->status ?: 'Stored') : 'Empty'}}</span>
</div>
@if($aliquot)
<div class="aliquot-number">{{$aliquot->aliquot_number ?: optional(optional($aliquot)->sample)->sample_code}}</div>
@else
<div class="position-meta">Empty</div>
@endif
</div>
@endforeach
</div>
</div>
<div class="mt-3 text-center">
<span class="mr-3"><span class="legend-dot"></span> Empty</span>
<span class="mr-3"><span class="legend-dot" style="background:#ebf9f0;border-color:#52b788;"></span> Occupied</span>
<span class="mr-3"><span class="legend-dot" style="background:#fff8e6;border-color:#f6ad2f;"></span> Reserved</span>
<span class="mr-3"><span class="legend-dot" style="background:#f4ebff;border-color:#9b5de5;"></span> Quarantine</span>
<span class="mr-3"><span class="legend-dot" style="background:#fee2e2;border-color:#ef4444;"></span> Destroyed</span>
</div>
</div>
<div class="d-flex align-items-center mt-3">
<button type="button" class="btn btn-outline-secondary btn-sm mr-4" id="btnClearSelectionBottom">Clear Selection</button>
<span class="mr-4">Total: <strong>{{$positions->count()}}</strong></span>
<span class="mr-4">Occupied: <strong class="text-success">{{$positions->where('occupied', 1)->count()}}</strong></span>
<span class="mr-4">Reserved: <strong class="text-warning">{{$positions->filter(fn($p) => optional($p->aliquot)->status === 'Reserved')->count()}}</strong></span>
<span>Empty: <strong>{{$positions->where('occupied', 0)->count()}}</strong></span>
</div>
@endif
</div>
</div>
<div class="col-xl-2 col-lg-12 mb-3">
<div class="selected-panel">
<div class="d-flex justify-content-between align-items-center">
<strong>Selected Position</strong>
<button type="button" class="btn btn-sm btn-link p-0" id="btnClearSelection">Clear</button>
</div>
<div class="selected-position-title mt-3" id="selected_position_code">-</div>
<span class="badge badge-light" id="selected_status">No selection</span>
<div class="selected-detail-label">Sample / Tube</div>
<div class="selected-detail-value" id="selected_tube">-</div>
<div class="selected-detail-label">Pathogen</div>
<div class="selected-detail-value" id="selected_pathogen">-</div>
<div class="selected-detail-label">Volume</div>
<div class="selected-detail-value" id="selected_volume">-</div>
<button type="button" class="btn btn-primary btn-block rounded-25 mt-4" id="btnManageSelected" disabled>Manage Position</button>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="hpis-card hpis-audit">
<div class="p-3">
<h6 class="mb-3">Audit Trail / Recent Movements</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0">
<thead>
<tr>
<th>Date & Time</th>
<th>Action</th>
<th>From</th>
<th>To</th>
<th>Sample / Tube</th>
<th>Performed By</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
@forelse($recentMovements as $movement)
<tr>
<td>{{$movement->created_at}}</td>
<td>{{empty($movement->from_position_id) ? 'Sample Stored' : (empty($movement->to_position_id) ? 'Removed' : 'Sample Moved')}}</td>
<td>{{$movement->fromPosition ? $movement->fromPosition->code : '-'}}</td>
<td>{{$movement->toPosition ? $movement->toPosition->code : '-'}}</td>
<td>{{optional($movement->aliquot)->aliquot_number ?: optional(optional($movement->aliquot)->sample)->sample_code}}</td>
<td>{{$movement->moved_by}}</td>
<td>{{$movement->movement_reason}}</td>
</tr>
@empty
<tr><td colspan="7" class="text-center text-muted">No recent movements found.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal mt-5 fade modal-primary" id="modal-aliquot">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">Storage Position</h6>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true" class="mdi mdi-close"></span></button>
</div>
<div class="modal-body">
<form method="post" action="#" id="form" onkeydown="return event.key != 'Enter';">
@csrf
<input type="hidden" name="storage_position_id" id="storage_position_id">
<input type="hidden" name="aliquot_id" id="aliquot_id">
<div id="occupied-summary" class="alert alert-danger" style="display:none;"></div>
<div id="movement-panel" style="display:none;">
<div class="row">
<div class="col-sm-12">
<div class="form-group mb-1">
<label>Move To Empty Position <span class="text-danger">*</span></label>
<select class="form-control select2" name="to_position_id" id="to_position_id" style="width: 100% !important;">
<option></option>
@foreach($emptyPositions as $emptyPosition)
<option value="{{$emptyPosition->id}}">
{{$emptyPosition->box_id == $box->id ? '[Current Box] ' : '[Other Freezer/Box] '}}
{{optional(optional(optional(optional(optional($emptyPosition->box)->rack)->equipment)->room)->building)->name}}
{{optional(optional(optional($emptyPosition->box)->rack)->equipment)->room ? ' / '.optional(optional(optional($emptyPosition->box)->rack)->equipment)->room->name : ''}}
{{optional(optional($emptyPosition->box)->rack)->equipment ? ' / '.optional(optional($emptyPosition->box)->rack)->equipment->name : ''}}
{{optional($emptyPosition->box)->rack ? ' / '.optional(optional($emptyPosition->box)->rack)->name : ''}}
{{optional($emptyPosition->box)->name ? ' / '.optional($emptyPosition->box)->name : ''}}
/ {{$emptyPosition->code}}
</option>
@endforeach
</select>
</div>
</div>
<div class="col-sm-12">
<div class="form-group mb-1">
<label>Movement Reason <span class="text-danger">*</span></label>
<textarea class="form-control" name="movement_reason" id="movement_reason" placeholder="e.g. freezer reorganization, sample retrieval, transfer to backup freezer"></textarea>
</div>
</div>
</div>
</div>
<div id="movement-history-panel" style="display:none;">
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Moved At</th>
<th>From</th>
<th>To</th>
<th>Reason</th>
<th>Moved By</th>
</tr>
</thead>
<tbody id="movement-history-body"></tbody>
</table>
</div>
</div>
<div id="use-volume-panel" style="display:none;">
<div class="alert alert-info">
<strong>Remaining now:</strong> <span id="current_remaining_volume"></span>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group mb-1">
<label>Volume Used <span class="text-danger">*</span></label>
<input type="number" step="0.01" class="form-control" name="used_volume" id="used_volume">
</div>
</div>
<div class="col-sm-8">
<div class="form-group mb-1">
<label>Reference Number</label>
<input type="text" class="form-control" name="reference_number" id="reference_number" placeholder="test request, extraction batch, etc.">
</div>
</div>
<div class="col-sm-12">
<div class="form-group mb-1">
<label>Reason for Use <span class="text-danger">*</span></label>
<textarea class="form-control" name="use_reason" id="use_reason" placeholder="e.g. used 30 mL for PCR extraction batch, QC retest, sequencing prep"></textarea>
</div>
</div>
</div>
</div>
<div id="empty-form">
<div class="row">
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Aliquot / Tube Number <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="aliquot_number" id="aliquot_number">
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Sample / Accession</label>
<select class="form-control select2" name="sample_id" id="sample_id" style="width: 100% !important;">
<option></option>
@foreach($samples as $sample)
<option value="{{$sample->id}}">
{{$sample->sample_code ?: $sample->accession_number}} {{optional($sample->pathogen)->scientific_name ? ' - '.optional($sample->pathogen)->scientific_name : ''}}
</option>
@endforeach
</select>
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Pathogen Name</label>
<select class="form-control select2" name="pathogen_id" id="pathogen_id" style="width: 100% !important;">
<option></option>
@foreach($pathogens as $pathogen)
<option value="{{$pathogen->id}}">
{{$pathogen->scientific_name}}{{$pathogen->common_name ? ' / '.$pathogen->common_name : ''}}
</option>
@endforeach
</select>
<small class="text-muted">Use this when no existing sample/accession is selected.</small>
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Sample Code</label>
<input type="text" class="form-control" name="sample_code" id="sample_code" placeholder="Optional; defaults to tube number">
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Container Type</label>
<select class="form-control select2" name="container_type_id" id="container_type_id" style="width: 100% !important;">
<option></option>
@foreach($containerTypes as $containerType)
<option value="{{$containerType->id}}">{{optional($containerType->containerName)->name}} {{$containerType->volume_ml ? '('.$containerType->volume_ml.' ml)' : ''}}</option>
@endforeach
</select>
</div>
</div>
<div class="col-sm-3">
<div class="form-group mb-1">
<label>Initial Volume <span class="text-danger">*</span></label>
<input type="number" step="0.01" class="form-control" name="initial_volume" id="initial_volume">
</div>
</div>
<div class="col-sm-3">
<div class="form-group mb-1">
<label>Remaining <span class="text-danger">*</span></label>
<input type="number" step="0.01" class="form-control" name="remaining_volume" id="remaining_volume">
</div>
</div>
<div class="col-sm-3">
<div class="form-group mb-1">
<label>Unit <span class="text-danger">*</span></label>
<select class="form-control" name="volume_unit" id="volume_unit">
<option value="µL">µL</option>
<option value="mL">mL</option>
</select>
</div>
</div>
<div class="col-sm-3">
<div class="form-group mb-1">
<label>Status <span class="text-danger">*</span></label>
<select class="form-control" name="status" id="status">
<option value="Available">Available</option>
<option value="Reserved">Reserved</option>
<option value="Quarantine">Quarantine</option>
</select>
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Concentration</label>
<input type="text" class="form-control" name="concentration" id="concentration">
</div>
</div>
<div class="col-sm-6">
<div class="form-group mb-1">
<label>Expiry Date</label>
<input type="date" class="form-control" name="expiry_date" id="expiry_date">
</div>
</div>
<div class="col-sm-12">
<div class="form-group mb-1">
<label>Description / Notes</label>
<textarea class="form-control" name="description" id="description"></textarea>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger rounded-25 mr-auto" id="btnRemoveAliquot" style="display:none;">Remove Aliquot</button>
<button type="button" class="btn btn-inverse-secondary rounded-25 mr-auto" id="btnBackActions" style="display:none;">Back</button>
<button type="button" class="btn btn-info rounded-25" id="btnShowHistory" style="display:none;">History</button>
<button type="button" class="btn btn-warning rounded-25" id="btnShowUseVolume" style="display:none;">Use Volume</button>
<button type="button" class="btn btn-primary rounded-25" id="btnShowMove" style="display:none;">Move</button>
<button type="button" class="btn btn-success rounded-25" id="btnMoveAliquot" style="display:none;">Confirm Move</button>
<button type="button" class="btn btn-success rounded-25" id="btnUseVolume" style="display:none;">Confirm Use</button>
<button type="button" class="btn btn-success rounded-25" id="btnSaveAliquot"><i class="fa fa-floppy-o"></i>&nbsp;&nbsp;Save Aliquot</button>
<button type="button" class="btn btn-secondary rounded-25 ml-2" data-dismiss='modal'>{{__('lang.cancel')}}</button>
</div>
</div>
</div>
</div>
@include('layout.footer')
</div>
</div>
</div>
@include('layout.common_script')
<script>
var generate_positions_url = "{{url('storage-position.generate')}}";
var save_aliquot_url = "{{url('storage-position.aliquot.save')}}";
var remove_aliquot_url = "{{url('storage-position.aliquot.remove')}}";
var move_aliquot_url = "{{url('storage-position.aliquot.move')}}";
var movement_history_url = "{{url('storage-position.aliquot.history')}}";
var use_volume_url = "{{url('storage-position.aliquot.use-volume')}}";
</script>
<script src="{{url(env("APP_URL").'storage/assets/js/admin/box_position.js?_').time()}}"></script>
</body>
</html>