changes to management
This commit is contained in:
156
app/Http/Controllers/BoxController.php
Normal file
156
app/Http/Controllers/BoxController.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\RecordStatusEnum;
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
use App\Models\Box;
|
||||||
|
use App\Models\Rack;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
class BoxController extends Controller
|
||||||
|
{
|
||||||
|
protected $model;
|
||||||
|
protected $rackModel;
|
||||||
|
protected $baseOrganizationId;
|
||||||
|
protected $base;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->baseOrganizationId = Session::get('base_organization_id');
|
||||||
|
$this->base = Session::get('base_organization');
|
||||||
|
$this->model = new Box();
|
||||||
|
$this->rackModel = new Rack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$recordStatusCondition = Auth::id() == 1 ? [RecordStatusEnum::DELETE, RecordStatusEnum::ACTIVE] : [RecordStatusEnum::ACTIVE];
|
||||||
|
$racks = $this->rackModel::with(['equipment.room.building'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->whereHas('equipment', function ($query) {
|
||||||
|
$query->where('organization_id', $this->baseOrganizationId);
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$boxes = $this->model::with(['rack.equipment.room.building'])
|
||||||
|
->whereIn(BaseModel::RECORD_STATUS_FIELD, $recordStatusCondition)
|
||||||
|
->whereIn('rack_id', $racks->pluck('id')->toArray())
|
||||||
|
->when(!empty($request->kword), function ($query) use ($request) {
|
||||||
|
$keyword = str_replace(' ', '', $request->kword);
|
||||||
|
$query->where(function ($query) use ($keyword) {
|
||||||
|
$query->whereRaw("replace(name, ' ','') like ?", ['%' . $keyword . '%'])
|
||||||
|
->orWhereRaw("replace(code, ' ','') like ?", ['%' . $keyword . '%']);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when(!empty(trim($request->rack_id)), function ($query) use ($request) {
|
||||||
|
$query->where('rack_id', $request->rack_id);
|
||||||
|
})
|
||||||
|
->orderBy(BaseModel::CREATED_AT_FIELD, 'desc')
|
||||||
|
->paginate(config('nrml.pagination.perpage', 10));
|
||||||
|
|
||||||
|
return view('box', ['boxes' => $boxes, 'racks' => $racks]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'rack_id' => 'required',
|
||||||
|
'code' => 'required',
|
||||||
|
'name' => 'required',
|
||||||
|
'rows' => 'required|integer|min:1',
|
||||||
|
'columns' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to create box.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model::query()->insert([
|
||||||
|
'rack_id' => $request->rack_id,
|
||||||
|
'code' => $request->code,
|
||||||
|
'name' => $request->name,
|
||||||
|
'rows' => $request->rows,
|
||||||
|
'columns' => $request->columns,
|
||||||
|
'capacity' => $request->capacity ?: ((int) $request->rows * (int) $request->columns),
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box has been created.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to create box.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'box_id' => 'required',
|
||||||
|
'rack_id' => 'required',
|
||||||
|
'code' => 'required',
|
||||||
|
'name' => 'required',
|
||||||
|
'rows' => 'required|integer|min:1',
|
||||||
|
'columns' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to update box.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model::query()->where('id', $request->box_id)->update([
|
||||||
|
'rack_id' => $request->rack_id,
|
||||||
|
'code' => $request->code,
|
||||||
|
'name' => $request->name,
|
||||||
|
'rows' => $request->rows,
|
||||||
|
'columns' => $request->columns,
|
||||||
|
'capacity' => $request->capacity ?: ((int) $request->rows * (int) $request->columns),
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box has been updated.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to update box.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$box = $this->model::query()->where('id', $request->box_id)->first();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box has been retrieved.', 'data' => $box]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to retrieve box.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->model::query()->where('id', $request->box_id)->update([
|
||||||
|
BaseModel::DELETED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::DELETED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::DELETE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box has been deleted.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to delete box.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->model::query()->where('id', $request->box_id)->update([BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE]);
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box has been restored.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to restore box.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
app/Http/Controllers/RackController.php
Normal file
146
app/Http/Controllers/RackController.php
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\RecordStatusEnum;
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
use App\Models\Equipment;
|
||||||
|
use App\Models\Rack;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
class RackController extends Controller
|
||||||
|
{
|
||||||
|
protected $model;
|
||||||
|
protected $equipmentModel;
|
||||||
|
protected $baseOrganizationId;
|
||||||
|
protected $base;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->baseOrganizationId = Session::get('base_organization_id');
|
||||||
|
$this->base = Session::get('base_organization');
|
||||||
|
$this->model = new Rack();
|
||||||
|
$this->equipmentModel = new Equipment();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$recordStatusCondition = Auth::id() == 1 ? [RecordStatusEnum::DELETE, RecordStatusEnum::ACTIVE] : [RecordStatusEnum::ACTIVE];
|
||||||
|
$equipments = $this->equipmentModel::with(['room.building'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->where('organization_id', $this->baseOrganizationId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$racks = $this->model::with(['equipment.room.building'])
|
||||||
|
->whereIn(BaseModel::RECORD_STATUS_FIELD, $recordStatusCondition)
|
||||||
|
->whereIn('equipment_id', $equipments->pluck('id')->toArray())
|
||||||
|
->when(!empty($request->kword), function ($query) use ($request) {
|
||||||
|
$keyword = str_replace(' ', '', $request->kword);
|
||||||
|
$query->where(function ($query) use ($keyword) {
|
||||||
|
$query->whereRaw("replace(name, ' ','') like ?", ['%' . $keyword . '%'])
|
||||||
|
->orWhereRaw("replace(code, ' ','') like ?", ['%' . $keyword . '%']);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when(!empty(trim($request->equipment_id)), function ($query) use ($request) {
|
||||||
|
$query->where('equipment_id', $request->equipment_id);
|
||||||
|
})
|
||||||
|
->orderBy(BaseModel::CREATED_AT_FIELD, 'desc')
|
||||||
|
->paginate(config('nrml.pagination.perpage', 10));
|
||||||
|
|
||||||
|
return view('rack', ['racks' => $racks, 'equipments' => $equipments]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'equipment_id' => 'required',
|
||||||
|
'code' => 'required',
|
||||||
|
'name' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to create rack.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model::query()->insert([
|
||||||
|
'equipment_id' => $request->equipment_id,
|
||||||
|
'code' => $request->code,
|
||||||
|
'name' => $request->name,
|
||||||
|
'description' => $request->description,
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Rack has been created.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to create rack.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'rack_id' => 'required',
|
||||||
|
'equipment_id' => 'required',
|
||||||
|
'code' => 'required',
|
||||||
|
'name' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to update rack.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model::query()->where('id', $request->rack_id)->update([
|
||||||
|
'equipment_id' => $request->equipment_id,
|
||||||
|
'code' => $request->code,
|
||||||
|
'name' => $request->name,
|
||||||
|
'description' => $request->description,
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Rack has been updated.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to update rack.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$rack = $this->model::query()->where('id', $request->rack_id)->first();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Rack has been retrieved.', 'data' => $rack]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to retrieve rack.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->model::query()->where('id', $request->rack_id)->update([
|
||||||
|
BaseModel::DELETED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::DELETED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::DELETE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Rack has been deleted.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to delete rack.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->model::query()->where('id', $request->rack_id)->update([BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE]);
|
||||||
|
return response()->json(['success' => true, 'message' => 'Rack has been restored.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to restore rack.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
472
app/Http/Controllers/StoragePositionController.php
Normal file
472
app/Http/Controllers/StoragePositionController.php
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\RecordStatusEnum;
|
||||||
|
use App\Models\AliqoutMovement;
|
||||||
|
use App\Models\Aliquot;
|
||||||
|
use App\Models\AliquotTransaction;
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
use App\Models\Box;
|
||||||
|
use App\Models\ContainerType;
|
||||||
|
use App\Models\Pathogen;
|
||||||
|
use App\Models\PathogenInventorySample;
|
||||||
|
use App\Models\Position;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
class StoragePositionController extends Controller
|
||||||
|
{
|
||||||
|
protected $baseOrganizationId;
|
||||||
|
protected $base;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->baseOrganizationId = Session::get('base_organization_id');
|
||||||
|
$this->base = Session::get('base_organization');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Box $box)
|
||||||
|
{
|
||||||
|
$box->load(['rack.equipment.room.building']);
|
||||||
|
|
||||||
|
$positions = Position::with(['aliquot.sample.pathogen', 'aliquot.containerType.containerName'])
|
||||||
|
->where('box_id', $box->id)
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->orderBy('row_label')
|
||||||
|
->orderByRaw('CAST(column_label AS UNSIGNED)')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$samples = PathogenInventorySample::with(['pathogen'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->where('organization_id', $this->baseOrganizationId)
|
||||||
|
->orderBy(BaseModel::CREATED_AT_FIELD, 'desc')
|
||||||
|
->limit(200)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$pathogens = Pathogen::with(['riskGroup', 'bsl'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->orderBy('scientific_name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$containerTypes = ContainerType::with(['containerName'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$emptyPositions = Position::with(['box.rack.equipment.room.building'])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->where('occupied', 0)
|
||||||
|
->orderByRaw('box_id = ? DESC', [$box->id])
|
||||||
|
->orderBy('box_id')
|
||||||
|
->orderBy('row_label')
|
||||||
|
->orderByRaw('CAST(column_label AS UNSIGNED)')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$recentMovements = AliqoutMovement::with([
|
||||||
|
'aliquot.sample.pathogen',
|
||||||
|
'fromPosition.box.rack.equipment.room.building',
|
||||||
|
'toPosition.box.rack.equipment.room.building',
|
||||||
|
])
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->where(function ($query) use ($box) {
|
||||||
|
$query->whereHas('fromPosition', function ($query) use ($box) {
|
||||||
|
$query->where('box_id', $box->id);
|
||||||
|
})->orWhereHas('toPosition', function ($query) use ($box) {
|
||||||
|
$query->where('box_id', $box->id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy(BaseModel::CREATED_AT_FIELD, 'desc')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('box_positions', [
|
||||||
|
'box' => $box,
|
||||||
|
'positions' => $positions,
|
||||||
|
'samples' => $samples,
|
||||||
|
'pathogens' => $pathogens,
|
||||||
|
'containerTypes' => $containerTypes,
|
||||||
|
'emptyPositions' => $emptyPositions,
|
||||||
|
'recentMovements' => $recentMovements,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate(['box_id' => 'required']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$box = Box::query()->findOrFail($request->box_id);
|
||||||
|
|
||||||
|
if ($box->rows < 1 || $box->columns < 1) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Box rows and columns must be configured before generating positions.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
for ($row = 1; $row <= $box->rows; $row++) {
|
||||||
|
$rowLabel = $this->rowLabel($row);
|
||||||
|
|
||||||
|
for ($column = 1; $column <= $box->columns; $column++) {
|
||||||
|
Position::query()->firstOrCreate(
|
||||||
|
[
|
||||||
|
'box_id' => $box->id,
|
||||||
|
'row_label' => $rowLabel,
|
||||||
|
'column_label' => (string) $column,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => $rowLabel . $column,
|
||||||
|
'occupied' => 0,
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Box positions generated successfully.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to generate box positions.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeAliquot(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'storage_position_id' => 'required',
|
||||||
|
'aliquot_number' => 'required',
|
||||||
|
'initial_volume' => 'required|numeric|min:0',
|
||||||
|
'remaining_volume' => 'required|numeric|min:0',
|
||||||
|
'volume_unit' => 'required',
|
||||||
|
'status' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to save aliquot.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$position = Position::with(['aliquot'])->findOrFail($request->storage_position_id);
|
||||||
|
if (!empty($position->aliquot)) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'This position is already occupied. Remove or transfer the current aliquot first.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$sampleId = $request->sample_id;
|
||||||
|
if (empty($sampleId) && !empty($request->pathogen_id)) {
|
||||||
|
$sampleId = PathogenInventorySample::query()->insertGetId([
|
||||||
|
'organization_id' => $this->baseOrganizationId,
|
||||||
|
'pathogen_id' => $request->pathogen_id,
|
||||||
|
'sample_code' => $request->sample_code ?: $request->aliquot_number,
|
||||||
|
'initial_volume' => $request->initial_volume,
|
||||||
|
'remaining_volume' => $request->remaining_volume,
|
||||||
|
'volume_unit' => $request->volume_unit,
|
||||||
|
'status' => 'Stored',
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliquotId = Aliquot::query()->insertGetId([
|
||||||
|
'sample_id' => $sampleId,
|
||||||
|
'container_type_id' => $request->container_type_id,
|
||||||
|
'aliquot_number' => $request->aliquot_number,
|
||||||
|
'initial_volume' => $request->initial_volume,
|
||||||
|
'remaining_volume' => $request->remaining_volume,
|
||||||
|
'volume_unit' => $request->volume_unit,
|
||||||
|
'concentration' => $request->concentration,
|
||||||
|
'storage_position_id' => $request->storage_position_id,
|
||||||
|
'expiry_date' => !empty($request->expiry_date) ? date('Y-m-d H:i:s', strtotime($request->expiry_date)) : null,
|
||||||
|
'status' => $request->status,
|
||||||
|
'last_accessed_at' => date('Y-m-d H:i:s'),
|
||||||
|
'description' => $request->description,
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$position->update([
|
||||||
|
'occupied' => 1,
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
AliqoutMovement::query()->insert([
|
||||||
|
'aliquot_id' => $aliquotId,
|
||||||
|
'from_position_id' => null,
|
||||||
|
'to_position_id' => $request->storage_position_id,
|
||||||
|
'movement_reason' => 'Initial storage placement',
|
||||||
|
'moved_by' => optional(Auth::user())->name ?? optional(Auth::user())->email ?? (string) Auth::id(),
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Aliquot placed in storage position.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to save aliquot.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAliquot(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'aliquot_id' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to remove aliquot.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliquot = Aliquot::query()->findOrFail($request->aliquot_id);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$positionId = $aliquot->storage_position_id;
|
||||||
|
$aliquot->update([
|
||||||
|
'status' => 'Removed',
|
||||||
|
BaseModel::DELETED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::DELETED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::DELETE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Position::query()->where('id', $positionId)->update([
|
||||||
|
'occupied' => 0,
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
AliqoutMovement::query()->insert([
|
||||||
|
'aliquot_id' => $aliquot->id,
|
||||||
|
'from_position_id' => $positionId,
|
||||||
|
'to_position_id' => null,
|
||||||
|
'movement_reason' => $request->movement_reason ?: 'Removed from storage',
|
||||||
|
'moved_by' => optional(Auth::user())->name ?? optional(Auth::user())->email ?? (string) Auth::id(),
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Aliquot removed from this position.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to remove aliquot.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function moveAliquot(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'aliquot_id' => 'required',
|
||||||
|
'to_position_id' => 'required',
|
||||||
|
'movement_reason' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to move aliquot.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliquot = Aliquot::query()
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->findOrFail($request->aliquot_id);
|
||||||
|
|
||||||
|
$fromPositionId = $aliquot->storage_position_id;
|
||||||
|
$toPosition = Position::with(['aliquot'])->findOrFail($request->to_position_id);
|
||||||
|
|
||||||
|
if ((int) $fromPositionId === (int) $toPosition->id) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'The aliquot is already in that position.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($toPosition->aliquot) || (int) $toPosition->occupied === 1) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Target position is occupied. Choose an empty position.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
Position::query()->where('id', $fromPositionId)->update([
|
||||||
|
'occupied' => 0,
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Position::query()->where('id', $toPosition->id)->update([
|
||||||
|
'occupied' => 1,
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$aliquot->update([
|
||||||
|
'storage_position_id' => $toPosition->id,
|
||||||
|
'last_accessed_at' => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
AliqoutMovement::query()->insert([
|
||||||
|
'aliquot_id' => $aliquot->id,
|
||||||
|
'from_position_id' => $fromPositionId,
|
||||||
|
'to_position_id' => $toPosition->id,
|
||||||
|
'movement_reason' => $request->movement_reason,
|
||||||
|
'moved_by' => optional(Auth::user())->name ?? optional(Auth::user())->email ?? (string) Auth::id(),
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Aliquot moved successfully.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to move aliquot.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function movementHistory(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'aliquot_id' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to retrieve movement history.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$movements = AliqoutMovement::with([
|
||||||
|
'fromPosition.box.rack.equipment.room.building',
|
||||||
|
'toPosition.box.rack.equipment.room.building',
|
||||||
|
])
|
||||||
|
->where('aliquot_id', $request->aliquot_id)
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->orderBy(BaseModel::CREATED_AT_FIELD, 'desc')
|
||||||
|
->get()
|
||||||
|
->map(function ($movement) {
|
||||||
|
return [
|
||||||
|
'from' => $this->positionLabel($movement->fromPosition),
|
||||||
|
'to' => $this->positionLabel($movement->toPosition),
|
||||||
|
'reason' => $movement->movement_reason,
|
||||||
|
'moved_by' => $movement->moved_by,
|
||||||
|
'moved_at' => $movement->created_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Movement history retrieved.', 'data' => $movements]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to retrieve movement history.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function useVolume(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validator = \Validator::make($request->all(), [
|
||||||
|
'aliquot_id' => 'required',
|
||||||
|
'used_volume' => 'required|numeric|min:0.01',
|
||||||
|
'use_reason' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to record aliquot use.', 'errors' => $validator->errors()->all()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliquot = Aliquot::query()
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, RecordStatusEnum::ACTIVE)
|
||||||
|
->findOrFail($request->aliquot_id);
|
||||||
|
|
||||||
|
$sourceVolume = (float) $aliquot->remaining_volume;
|
||||||
|
$usedVolume = (float) $request->used_volume;
|
||||||
|
|
||||||
|
if ($usedVolume > $sourceVolume) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Used volume cannot be greater than remaining volume.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$remainingVolume = $sourceVolume - $usedVolume;
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$aliquot->update([
|
||||||
|
'remaining_volume' => $remainingVolume,
|
||||||
|
'status' => $remainingVolume <= 0 ? 'Consumed' : $aliquot->status,
|
||||||
|
'last_accessed_at' => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::UPDATED_BY_FIELD => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
AliquotTransaction::query()->insert([
|
||||||
|
'aliquot_id' => $aliquot->id,
|
||||||
|
'transaction_at' => date('Y-m-d H:i:s'),
|
||||||
|
'performed_by' => optional(Auth::user())->name ?? optional(Auth::user())->email ?? (string) Auth::id(),
|
||||||
|
'source_volumn' => $sourceVolume,
|
||||||
|
'affected_volume' => $usedVolume,
|
||||||
|
'remaining_volume' => $remainingVolume,
|
||||||
|
'volume_unit' => $aliquot->volume_unit,
|
||||||
|
'storage_position_id' => $aliquot->storage_position_id,
|
||||||
|
'reference_number' => $request->reference_number,
|
||||||
|
'description' => $request->use_reason,
|
||||||
|
'state' => 'approved',
|
||||||
|
BaseModel::CREATED_AT_FIELD => date('Y-m-d H:i:s'),
|
||||||
|
BaseModel::CREATED_BY_FIELD => Auth::id(),
|
||||||
|
BaseModel::RECORD_STATUS_FIELD => RecordStatusEnum::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Aliquot use recorded. Remaining volume: ' . $remainingVolume . ' ' . $aliquot->volume_unit]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unable to record aliquot use.', 'errors' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rowLabel(int $row): string
|
||||||
|
{
|
||||||
|
$label = '';
|
||||||
|
|
||||||
|
while ($row > 0) {
|
||||||
|
$row--;
|
||||||
|
$label = chr(65 + ($row % 26)) . $label;
|
||||||
|
$row = intdiv($row, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function positionLabel($position): string
|
||||||
|
{
|
||||||
|
if (empty($position)) {
|
||||||
|
return 'Outside storage';
|
||||||
|
}
|
||||||
|
|
||||||
|
$box = $position->box;
|
||||||
|
$rack = optional($box)->rack;
|
||||||
|
$equipment = optional($rack)->equipment;
|
||||||
|
$room = optional($equipment)->room;
|
||||||
|
$building = optional($room)->building;
|
||||||
|
|
||||||
|
return trim(
|
||||||
|
collect([
|
||||||
|
optional($building)->name,
|
||||||
|
optional($room)->name,
|
||||||
|
optional($equipment)->name,
|
||||||
|
optional($rack)->name,
|
||||||
|
optional($box)->name,
|
||||||
|
$position->code,
|
||||||
|
])->filter()->implode(' / ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Models/AliqoutMovement.php
Normal file
56
app/Models/AliqoutMovement.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class AliqoutMovement extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database schema currently uses the legacy misspelling "aliqout".
|
||||||
|
*/
|
||||||
|
protected $table = 'aliqout_movements';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'aliquot_id',
|
||||||
|
'from_position_id',
|
||||||
|
'to_position_id',
|
||||||
|
'movement_reason',
|
||||||
|
'moved_by',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aliquot()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Aliquot::class, 'aliquot_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromPosition()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Position::class, 'from_position_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toPosition()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Position::class, 'to_position_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Models/Aliquot.php
Normal file
72
app/Models/Aliquot.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Aliquot extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
protected $table = 'aliquots';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'sample_id',
|
||||||
|
'container_type_id',
|
||||||
|
'aliquot_type_concept_id',
|
||||||
|
'parent_aliquot_id',
|
||||||
|
'aliquot_number',
|
||||||
|
'initial_volume',
|
||||||
|
'remaining_volume',
|
||||||
|
'volume_unit',
|
||||||
|
'concentration',
|
||||||
|
'storage_position_id',
|
||||||
|
'expiry_date',
|
||||||
|
'status',
|
||||||
|
'last_accessed_at',
|
||||||
|
'description',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function position()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Position::class, 'storage_position_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sample()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PathogenInventorySample::class, 'sample_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function containerType()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ContainerType::class, 'container_type_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function movements()
|
||||||
|
{
|
||||||
|
return $this->hasMany(AliqoutMovement::class, 'aliquot_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transactions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(AliquotTransaction::class, 'aliquot_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Models/AliquotTransaction.php
Normal file
55
app/Models/AliquotTransaction.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class AliquotTransaction extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
protected $table = 'aliquot_transactions';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'aliquot_id',
|
||||||
|
'transaction_type_concept_id',
|
||||||
|
'transaction_at',
|
||||||
|
'performed_by',
|
||||||
|
'source_volumn',
|
||||||
|
'affected_volume',
|
||||||
|
'remaining_volume',
|
||||||
|
'volume_unit',
|
||||||
|
'storage_position_id',
|
||||||
|
'reference_number',
|
||||||
|
'description',
|
||||||
|
'state',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aliquot()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Aliquot::class, 'aliquot_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function position()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Position::class, 'storage_position_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Models/Box.php
Normal file
56
app/Models/Box.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Box extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* Note: the current database schema uses the legacy table name "boxs".
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'boxs';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'rack_id',
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'rows',
|
||||||
|
'columns',
|
||||||
|
'capacity',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rack()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Rack::class, 'rack_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function positions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Position::class, 'box_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,5 +36,12 @@ class Building extends BaseModel
|
|||||||
parent::boot();
|
parent::boot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function organization(){
|
||||||
|
return $this->belongsTo(Organization::class, 'organization_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rooms(){
|
||||||
|
return $this->hasMany(Room::class, 'building_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,5 +46,8 @@ class Equipment extends BaseModel
|
|||||||
return $this->belongsTo(Room::class, 'room_id', 'id');
|
return $this->belongsTo(Room::class, 'room_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function racks(){
|
||||||
|
return $this->hasMany(Rack::class, 'equipment_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,8 @@ class Organization extends BaseModel
|
|||||||
// return $this->hasMany('App\Models\UserOrganization', 'organization_id','id')->where('record_status_id', RecordStatusEnum::ACTIVE)->where('organization_id', Session::get('base_organization_id'));
|
// return $this->hasMany('App\Models\UserOrganization', 'organization_id','id')->where('record_status_id', RecordStatusEnum::ACTIVE)->where('organization_id', Session::get('base_organization_id'));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
public function buildings(){
|
||||||
|
return $this->hasMany(Building::class, 'organization_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
62
app/Models/PathogenInventorySample.php
Normal file
62
app/Models/PathogenInventorySample.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class PathogenInventorySample extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
protected $table = 'samples';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'organization_id',
|
||||||
|
'pathogen_id',
|
||||||
|
'sample_type_concept_id',
|
||||||
|
'container_type_id',
|
||||||
|
'risk_group_concept_id',
|
||||||
|
'biosafety_level_concept_id',
|
||||||
|
'sample_code',
|
||||||
|
'accession_number',
|
||||||
|
'soc_concept_id',
|
||||||
|
'collected_at',
|
||||||
|
'collected_by',
|
||||||
|
'received_date',
|
||||||
|
'received_by',
|
||||||
|
'initial_volume',
|
||||||
|
'volume_unit',
|
||||||
|
'remaining_volume',
|
||||||
|
'is_own',
|
||||||
|
'owner_organization_id',
|
||||||
|
'status',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pathogen()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Pathogen::class, 'pathogen_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aliquots()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Aliquot::class, 'sample_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Models/Position.php
Normal file
49
app/Models/Position.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Position extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
protected $table = 'positions';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'box_id',
|
||||||
|
'code',
|
||||||
|
'row_label',
|
||||||
|
'column_label',
|
||||||
|
'occupied',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function box()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Box::class, 'box_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aliquot()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Aliquot::class, 'storage_position_id', 'id')
|
||||||
|
->where(BaseModel::RECORD_STATUS_FIELD, BaseModel::RECORD_STATUS_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/Models/Rack.php
Normal file
52
app/Models/Rack.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\AuditLogTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Rack extends BaseModel
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
use HasFactory;
|
||||||
|
use AuditLogTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'racks';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'equipment_id',
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
BaseModel::CREATED_AT_FIELD,
|
||||||
|
BaseModel::CREATED_BY_FIELD,
|
||||||
|
BaseModel::UPDATED_AT_FIELD,
|
||||||
|
BaseModel::UPDATED_BY_FIELD,
|
||||||
|
BaseModel::DELETED_AT_FIELD,
|
||||||
|
BaseModel::DELETED_BY_FIELD,
|
||||||
|
BaseModel::RECORD_STATUS_FIELD,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equipment()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Equipment::class, 'equipment_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boxes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Box::class, 'rack_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,5 +44,8 @@ class Room extends BaseModel
|
|||||||
return $this->belongsTo(Concept::class, 'biosafety_level_concept_id', 'id');
|
return $this->belongsTo(Concept::class, 'biosafety_level_concept_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function equipments(){
|
||||||
|
return $this->hasMany(Equipment::class, 'room_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,18 @@ return [
|
|||||||
'sample_entry_by' => 'Entered By',
|
'sample_entry_by' => 'Entered By',
|
||||||
'modify_by' => 'Modified By',
|
'modify_by' => 'Modified By',
|
||||||
|
|
||||||
'code' => 'Code'
|
'code' => 'Code',
|
||||||
|
'name' => 'Name',
|
||||||
|
'rack' => 'Rack',
|
||||||
|
'rows' => 'Rows',
|
||||||
|
'columns' => 'Columns',
|
||||||
|
'capacity' => 'Capacity',
|
||||||
|
'box_name' => 'Box Name',
|
||||||
|
'rack_name' => 'Rack Name',
|
||||||
|
'freezer' => 'Freezer',
|
||||||
|
'description' => 'Description',
|
||||||
|
'sample_box' => 'Sample Boxes',
|
||||||
|
'sample_rack' => 'Sample Racks'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,5 +54,18 @@ return [
|
|||||||
'create_sample' => 'Create Sample',
|
'create_sample' => 'Create Sample',
|
||||||
'appointment' => 'Appointments',
|
'appointment' => 'Appointments',
|
||||||
'view_sample' => 'Sample List',
|
'view_sample' => 'Sample List',
|
||||||
|
'aliquot_management' => 'Aliquot Management',
|
||||||
|
'incident_records' => 'Incident Records',
|
||||||
|
'incident_table' => 'Incident Table',
|
||||||
|
'incident_type' => 'Incident Types',
|
||||||
|
'management' => 'Management',
|
||||||
|
'building' => 'Buildings',
|
||||||
|
'safe_room' => 'Safety Rooms',
|
||||||
|
'freezer' => 'Freezers',
|
||||||
|
'rack' => 'Racks',
|
||||||
|
'sample_box' => 'Sample Boxes',
|
||||||
|
'reference_lists' => 'Reference Lists',
|
||||||
|
'pathogen_type' => 'Pathogen Types / Toxins',
|
||||||
|
'sample_container_type' => 'Sample Container Types',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ return [
|
|||||||
'sample_entry_by' => 'បញ្ចូលលទ្ធផលដោយ',
|
'sample_entry_by' => 'បញ្ចូលលទ្ធផលដោយ',
|
||||||
'modify_by' => 'កែប្រែលទ្ធផលដោយ',
|
'modify_by' => 'កែប្រែលទ្ធផលដោយ',
|
||||||
|
|
||||||
'code' => 'លេខកូដ'
|
'code' => 'លេខកូដ',
|
||||||
|
'name' => 'ឈ្មោះ',
|
||||||
|
'rack' => 'ធ្នើ',
|
||||||
|
'rows' => 'ជួរដេក',
|
||||||
|
'columns' => 'ជួរឈរ',
|
||||||
|
'capacity' => 'ចំណុះ',
|
||||||
|
'box_name' => 'ឈ្មោះប្រអប់',
|
||||||
|
'rack_name' => 'ឈ្មោះធ្នើ',
|
||||||
|
'freezer' => 'ទូររក្សាសំណាក',
|
||||||
|
'description' => 'បរិយាយ',
|
||||||
|
'sample_box' => 'ប្រអប់សំណាក',
|
||||||
|
'sample_rack' => 'ធ្នើរក្សាសំណាក'
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -53,7 +53,20 @@ return [
|
|||||||
'payment'=> 'តារាងបង់ប្រាក់',
|
'payment'=> 'តារាងបង់ប្រាក់',
|
||||||
'sample_management' => 'គ្រប់គ្រងសំណាក',
|
'sample_management' => 'គ្រប់គ្រងសំណាក',
|
||||||
'create_sample' => 'បញ្ចូលសំណាកថ្មី',
|
'create_sample' => 'បញ្ចូលសំណាកថ្មី',
|
||||||
'view_sample' => 'តារាងសំណាក'
|
'view_sample' => 'តារាងសំណាក',
|
||||||
|
'aliquot_management' => 'គ្រប់គ្រង Aliquots',
|
||||||
|
'incident_records' => 'កត់ត្រាឧប្បត្តិហេតុ',
|
||||||
|
'incident_table' => 'តារាងឧប្បត្តិហេតុ',
|
||||||
|
'incident_type' => 'ប្រភេទឧប្បត្តិហេតុ',
|
||||||
|
'management' => 'គ្រប់គ្រង',
|
||||||
|
'building' => 'អគារ',
|
||||||
|
'safe_room' => 'បន្ទប់សុវត្ថិភាព',
|
||||||
|
'freezer' => 'ទូររក្សាសំណាក',
|
||||||
|
'rack' => 'ធ្នើ',
|
||||||
|
'sample_box' => 'ប្រអប់សំណាក',
|
||||||
|
'reference_lists' => 'បញ្ជីគម្រូ',
|
||||||
|
'pathogen_type' => 'ប្រភេទមេរោគ/ជាតិពុល',
|
||||||
|
'sample_container_type' => 'ប្រភេទទីបផ្ទុកសំណាក'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
112
public/storage/assets/js/admin/box.js
Normal file
112
public/storage/assets/js/admin/box.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
$("#btnSave").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
let box_id = $('#box_id').val();
|
||||||
|
url = parseInt(box_id) === 0 ? save_url : update_url;
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
box_id: box_id,
|
||||||
|
rack_id: $('#rack_id').val(),
|
||||||
|
code: $('#code').val(),
|
||||||
|
name: $('#name').val(),
|
||||||
|
rows: $('#rows').val(),
|
||||||
|
columns: $('#columns').val(),
|
||||||
|
capacity: $('#capacity').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function delete_lab(box_id){
|
||||||
|
Swal.fire({
|
||||||
|
text: confirm_delete_record,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonClass: 'danger',
|
||||||
|
confirmButtonText: confirm_ok_delete,
|
||||||
|
cancelButtonText: confirm_cancel
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
$.ajax({
|
||||||
|
url: delete_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
box_id: box_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success) { setTimeout(function () {location.reload()}, messageDuration) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore_lab(box_id){
|
||||||
|
Swal.fire({
|
||||||
|
text: confirm_restore_record,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonClass: 'danger',
|
||||||
|
confirmButtonText: confirm_ok_restore,
|
||||||
|
cancelButtonText: confirm_cancel_restore,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
$.ajax({
|
||||||
|
url: restore_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
box_id: box_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success) { setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modal-box').on('show.bs.modal', function (event) {
|
||||||
|
var button = $(event.relatedTarget)
|
||||||
|
var action = button.data('action')
|
||||||
|
var modal = $(this);
|
||||||
|
if(action === "new"){
|
||||||
|
modal.find('.modal-body input').val("");
|
||||||
|
modal.find('.modal-body select[name="rack_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body input[name="box_id"]').val(0);
|
||||||
|
modal.find('.modal-title').html(modal_add_title);
|
||||||
|
}else if(action === "edit"){
|
||||||
|
box_id = button.data('box_id');
|
||||||
|
modal.find('.modal-body input[name="box_id"]').val(box_id);
|
||||||
|
modal.find('.modal-title').html(modal_edit_title);
|
||||||
|
$.ajax({
|
||||||
|
url: get_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
box_id: box_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
if(res.success) {
|
||||||
|
let data = res.data;
|
||||||
|
modal.find('.modal-body input[name="code"]').val(data.code);
|
||||||
|
modal.find('.modal-body input[name="name"]').val(data.name);
|
||||||
|
modal.find('.modal-body input[name="rows"]').val(data.rows);
|
||||||
|
modal.find('.modal-body input[name="columns"]').val(data.columns);
|
||||||
|
modal.find('.modal-body input[name="capacity"]').val(data.capacity);
|
||||||
|
modal.find('.modal-body select[name="rack_id"]').val(data.rack_id).trigger('change');
|
||||||
|
} else{
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
308
public/storage/assets/js/admin/box_position.js
Normal file
308
public/storage/assets/js/admin/box_position.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
$("#btnGeneratePositions").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
let box_id = $(this).data('box_id');
|
||||||
|
$.ajax({
|
||||||
|
url: generate_positions_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
box_id: box_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedPositionCard = null;
|
||||||
|
|
||||||
|
function setActionButtonsDefaultForOccupied()
|
||||||
|
{
|
||||||
|
$("#btnRemoveAliquot").show();
|
||||||
|
$("#btnShowMove").show();
|
||||||
|
$("#btnShowHistory").show();
|
||||||
|
$("#btnShowUseVolume").show();
|
||||||
|
$("#btnBackActions").hide();
|
||||||
|
$("#btnMoveAliquot").hide();
|
||||||
|
$("#btnUseVolume").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPositionModal(card)
|
||||||
|
{
|
||||||
|
if (card === undefined || !card.length) {
|
||||||
|
card = selectedPositionCard;
|
||||||
|
}
|
||||||
|
if (!card || !card.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal = $("#modal-aliquot");
|
||||||
|
let aliquotId = card.data('aliquot_id');
|
||||||
|
|
||||||
|
modal.find('.modal-title').html('Storage Position ' + card.data('position_code'));
|
||||||
|
modal.find('input[name="storage_position_id"]').val(card.data('position_id'));
|
||||||
|
modal.find('input[name="aliquot_id"]').val(aliquotId || '');
|
||||||
|
|
||||||
|
if (aliquotId) {
|
||||||
|
$("#empty-form").hide();
|
||||||
|
$("#btnSaveAliquot").hide();
|
||||||
|
setActionButtonsDefaultForOccupied();
|
||||||
|
$("#movement-panel").hide();
|
||||||
|
$("#movement-history-panel").hide();
|
||||||
|
$("#use-volume-panel").hide();
|
||||||
|
$("#movement-history-body").html('');
|
||||||
|
modal.find('.modal-body select[name="to_position_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body textarea[name="movement_reason"]').val("");
|
||||||
|
modal.find('.modal-body input[name="used_volume"]').val("");
|
||||||
|
modal.find('.modal-body input[name="reference_number"]').val("");
|
||||||
|
modal.find('.modal-body textarea[name="use_reason"]').val("");
|
||||||
|
$("#current_remaining_volume").text((card.data('remaining_volume') || '0') + ' ' + (card.data('volume_unit') || ''));
|
||||||
|
$("#occupied-summary").show().html(
|
||||||
|
'<strong>Occupied:</strong> ' + (card.data('aliquot_number') || '') +
|
||||||
|
'<br><strong>Sample:</strong> ' + (card.data('sample') || '-') +
|
||||||
|
'<br><strong>Pathogen:</strong> ' + (card.data('pathogen') || '-') +
|
||||||
|
'<br><strong>Remaining:</strong> ' + (card.data('remaining_volume') || '0') + ' ' + (card.data('volume_unit') || '') +
|
||||||
|
'<br><strong>Status:</strong> ' + (card.data('status') || '-')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$("#occupied-summary").hide().html('');
|
||||||
|
$("#empty-form").show();
|
||||||
|
$("#btnSaveAliquot").show();
|
||||||
|
$("#btnRemoveAliquot").hide();
|
||||||
|
$("#btnShowMove").hide();
|
||||||
|
$("#btnShowHistory").hide();
|
||||||
|
$("#btnShowUseVolume").hide();
|
||||||
|
$("#btnBackActions").hide();
|
||||||
|
$("#btnMoveAliquot").hide();
|
||||||
|
$("#btnUseVolume").hide();
|
||||||
|
$("#movement-panel").hide();
|
||||||
|
$("#movement-history-panel").hide();
|
||||||
|
$("#use-volume-panel").hide();
|
||||||
|
$("#movement-history-body").html('');
|
||||||
|
modal.find('.modal-body input[type="text"], .modal-body input[type="number"], .modal-body input[type="date"], .modal-body textarea').val("");
|
||||||
|
modal.find('.modal-body select[name="sample_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body select[name="pathogen_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body select[name="container_type_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body select[name="volume_unit"]').val("µL");
|
||||||
|
modal.find('.modal-body select[name="status"]').val("Available");
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.modal({backdrop: 'static'});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".position-card").on('click', function(){
|
||||||
|
let card = $(this);
|
||||||
|
selectedPositionCard = card;
|
||||||
|
$(".position-card").removeClass("selected");
|
||||||
|
card.addClass("selected");
|
||||||
|
|
||||||
|
$("#selected_position_code").text(card.data('position_code') || '-');
|
||||||
|
$("#selected_status").text(card.data('status') || 'Empty')
|
||||||
|
.removeClass('badge-light badge-success badge-warning badge-danger')
|
||||||
|
.addClass(card.data('aliquot_id') ? 'badge-success' : 'badge-light');
|
||||||
|
$("#selected_tube").text(card.data('aliquot_number') || card.data('sample') || '-');
|
||||||
|
$("#selected_pathogen").text(card.data('pathogen') || '-');
|
||||||
|
let volume = card.data('remaining_volume') ? (card.data('remaining_volume') + ' ' + (card.data('volume_unit') || '')) : '-';
|
||||||
|
$("#selected_volume").text(volume);
|
||||||
|
$("#btnManageSelected").prop('disabled', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".position-card").on('dblclick', function(){
|
||||||
|
selectedPositionCard = $(this);
|
||||||
|
openPositionModal(selectedPositionCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnManageSelected").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
openPositionModal(selectedPositionCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnClearSelection").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
selectedPositionCard = null;
|
||||||
|
$(".position-card").removeClass("selected");
|
||||||
|
$("#selected_position_code").text('-');
|
||||||
|
$("#selected_status").text('No selection').removeClass('badge-success badge-warning badge-danger').addClass('badge-light');
|
||||||
|
$("#selected_tube").text('-');
|
||||||
|
$("#selected_pathogen").text('-');
|
||||||
|
$("#selected_volume").text('-');
|
||||||
|
$("#btnManageSelected").prop('disabled', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnSaveAliquot").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
url: save_aliquot_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
storage_position_id: $('#storage_position_id').val(),
|
||||||
|
sample_id: $('#sample_id').val(),
|
||||||
|
pathogen_id: $('#pathogen_id').val(),
|
||||||
|
sample_code: $('#sample_code').val(),
|
||||||
|
container_type_id: $('#container_type_id').val(),
|
||||||
|
aliquot_number: $('#aliquot_number').val(),
|
||||||
|
initial_volume: $('#initial_volume').val(),
|
||||||
|
remaining_volume: $('#remaining_volume').val(),
|
||||||
|
volume_unit: $('#volume_unit').val(),
|
||||||
|
concentration: $('#concentration').val(),
|
||||||
|
expiry_date: $('#expiry_date').val(),
|
||||||
|
status: $('#status').val(),
|
||||||
|
description: $('#description').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnShowMove").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$("#movement-history-panel").hide();
|
||||||
|
$("#use-volume-panel").hide();
|
||||||
|
$("#btnUseVolume").hide();
|
||||||
|
$("#btnShowMove").hide();
|
||||||
|
$("#btnShowHistory").hide();
|
||||||
|
$("#btnShowUseVolume").hide();
|
||||||
|
$("#btnRemoveAliquot").hide();
|
||||||
|
$("#btnBackActions").show();
|
||||||
|
$("#movement-panel").show();
|
||||||
|
$("#btnMoveAliquot").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnMoveAliquot").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
url: move_aliquot_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
aliquot_id: $('#aliquot_id').val(),
|
||||||
|
to_position_id: $('#to_position_id').val(),
|
||||||
|
movement_reason: $('#movement_reason').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnShowHistory").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$("#movement-panel").hide();
|
||||||
|
$("#use-volume-panel").hide();
|
||||||
|
$("#btnMoveAliquot").hide();
|
||||||
|
$("#btnUseVolume").hide();
|
||||||
|
$("#btnShowMove").hide();
|
||||||
|
$("#btnShowHistory").hide();
|
||||||
|
$("#btnShowUseVolume").hide();
|
||||||
|
$("#btnRemoveAliquot").hide();
|
||||||
|
$("#btnBackActions").show();
|
||||||
|
$.ajax({
|
||||||
|
url: movement_history_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
aliquot_id: $('#aliquot_id').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
if(res.success) {
|
||||||
|
let rows = '';
|
||||||
|
if (res.data.length === 0) {
|
||||||
|
rows = '<tr><td colspan="5" class="text-center text-muted">No movement history found.</td></tr>';
|
||||||
|
} else {
|
||||||
|
res.data.forEach(function(item){
|
||||||
|
rows += '<tr>' +
|
||||||
|
'<td>' + (item.moved_at || '-') + '</td>' +
|
||||||
|
'<td>' + (item.from || '-') + '</td>' +
|
||||||
|
'<td>' + (item.to || '-') + '</td>' +
|
||||||
|
'<td>' + (item.reason || '-') + '</td>' +
|
||||||
|
'<td>' + (item.moved_by || '-') + '</td>' +
|
||||||
|
'</tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$("#movement-history-body").html(rows);
|
||||||
|
$("#movement-history-panel").show();
|
||||||
|
} else {
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnShowUseVolume").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$("#movement-panel").hide();
|
||||||
|
$("#movement-history-panel").hide();
|
||||||
|
$("#btnMoveAliquot").hide();
|
||||||
|
$("#btnShowMove").hide();
|
||||||
|
$("#btnShowHistory").hide();
|
||||||
|
$("#btnShowUseVolume").hide();
|
||||||
|
$("#btnRemoveAliquot").hide();
|
||||||
|
$("#btnBackActions").show();
|
||||||
|
$("#use-volume-panel").show();
|
||||||
|
$("#btnUseVolume").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnBackActions").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$("#movement-panel").hide();
|
||||||
|
$("#movement-history-panel").hide();
|
||||||
|
$("#use-volume-panel").hide();
|
||||||
|
$("#btnMoveAliquot").hide();
|
||||||
|
$("#btnUseVolume").hide();
|
||||||
|
$("#btnBackActions").hide();
|
||||||
|
$("#btnRemoveAliquot").show();
|
||||||
|
$("#btnShowMove").show();
|
||||||
|
$("#btnShowHistory").show();
|
||||||
|
$("#btnShowUseVolume").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnUseVolume").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
url: use_volume_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
aliquot_id: $('#aliquot_id').val(),
|
||||||
|
used_volume: $('#used_volume').val(),
|
||||||
|
reference_number: $('#reference_number').val(),
|
||||||
|
use_reason: $('#use_reason').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnRemoveAliquot").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
Swal.fire({
|
||||||
|
text: 'Remove this aliquot from the storage position?',
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonClass: 'danger',
|
||||||
|
confirmButtonText: confirm_ok_delete,
|
||||||
|
cancelButtonText: confirm_cancel
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
$.ajax({
|
||||||
|
url: remove_aliquot_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
aliquot_id: $('#aliquot_id').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success) { setTimeout(function () {location.reload()}, messageDuration) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
109
public/storage/assets/js/admin/rack.js
Normal file
109
public/storage/assets/js/admin/rack.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
$("#btnSave").on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
let rack_id = $('#rack_id').val();
|
||||||
|
url = parseInt(rack_id) === 0 ? save_url : update_url;
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
rack_id: rack_id,
|
||||||
|
equipment_id: $('#equipment_id').val(),
|
||||||
|
code: $('#code').val(),
|
||||||
|
name: $('#name').val(),
|
||||||
|
description: $('#description').val(),
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success){ setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function delete_lab(rack_id){
|
||||||
|
Swal.fire({
|
||||||
|
text: confirm_delete_record,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonClass: 'danger',
|
||||||
|
confirmButtonText: confirm_ok_delete,
|
||||||
|
cancelButtonText: confirm_cancel
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
$.ajax({
|
||||||
|
url: delete_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
rack_id: rack_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success) { setTimeout(function () {location.reload()}, messageDuration) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore_lab(rack_id){
|
||||||
|
Swal.fire({
|
||||||
|
text: confirm_restore_record,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonClass: 'danger',
|
||||||
|
confirmButtonText: confirm_ok_restore,
|
||||||
|
cancelButtonText: confirm_cancel_restore,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
$.ajax({
|
||||||
|
url: restore_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
rack_id: rack_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
if(res.success) { setTimeout(function () {location.reload()}, messageDuration)}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modal-rack').on('show.bs.modal', function (event) {
|
||||||
|
var button = $(event.relatedTarget)
|
||||||
|
var action = button.data('action')
|
||||||
|
var modal = $(this);
|
||||||
|
if(action === "new"){
|
||||||
|
modal.find('.modal-body input').val("");
|
||||||
|
modal.find('.modal-body textarea').val("");
|
||||||
|
modal.find('.modal-body select[name="equipment_id"]').val("").trigger('change');
|
||||||
|
modal.find('.modal-body input[name="rack_id"]').val(0);
|
||||||
|
modal.find('.modal-title').html(modal_add_title);
|
||||||
|
}else if(action === "edit"){
|
||||||
|
rack_id = button.data('rack_id');
|
||||||
|
modal.find('.modal-body input[name="rack_id"]').val(rack_id);
|
||||||
|
modal.find('.modal-title').html(modal_edit_title);
|
||||||
|
$.ajax({
|
||||||
|
url: get_url,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
"_token": csrf_token,
|
||||||
|
rack_id: rack_id,
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
if(res.success) {
|
||||||
|
let data = res.data;
|
||||||
|
modal.find('.modal-body input[name="code"]').val(data.code);
|
||||||
|
modal.find('.modal-body input[name="name"]').val(data.name);
|
||||||
|
modal.find('.modal-body textarea[name="description"]').val(data.description);
|
||||||
|
modal.find('.modal-body select[name="equipment_id"]').val(data.equipment_id).trigger('change');
|
||||||
|
} else{
|
||||||
|
handleMessage(res.success, res.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
196
resources/views/box.blade.php
Normal file
196
resources/views/box.blade.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
@include('layout.header')
|
||||||
|
<style>
|
||||||
|
.box-action-buttons {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-action-buttons .btn {
|
||||||
|
width: 34px;
|
||||||
|
height: 30px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-action-buttons .btn i {
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</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="row">
|
||||||
|
<div class="col-md-12 grid-margin stretch-card">
|
||||||
|
<div class="card" style="min-height: 80vh;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title"><i class="mdi mdi-dots-vertical menu-icon"></i> {{__('general.sample_box')}}</h4>
|
||||||
|
<form method="get" action="{{url('box/search')}}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-12 col-md-9 col-lg-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control form-control-md rounded-left-25 rounded-right-0" value="{{isset($_GET['kword']) ? $_GET['kword'] : ''}}" name="kword" id="kword" placeholder="{{__('laboratory.search_placeholder')}}">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<select class="form-control select2 rounded-left-0 rounded-right-0" name="rack_id" style="width: 280px !important;">
|
||||||
|
<option></option>
|
||||||
|
@foreach($racks as $rack)
|
||||||
|
<option {{@$_GET['rack_id']==$rack->id ? 'selected':''}} value="{{$rack->id}}">
|
||||||
|
{{optional($rack->equipment)->name}} / {{$rack->name}}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-md btn-inverse-secondary rounded-right-25 px-3" type="submit"><i class="fa fa-search"></i> {{__('general.btn_search')}}</button>
|
||||||
|
<button class="btn btn-md btn-inverse-success waves-effect rounded-25 px-4 ml-3" type="button" data-action="new" data-toggle="modal" data-target="#modal-box" data-backdrop="static"><i class="typcn typcn-plus"></i> {{__('general.btn_add_new')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="col-sm-12 p-0 mb-1 mt-3">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gradient-light">
|
||||||
|
<th class="py-2" width="50px">{{__('laboratory.table_no')}}</th>
|
||||||
|
<th class="py-2 text-center" width="150px">{{__('laboratory.table_action')}}</th>
|
||||||
|
<th class="py-2" width="120px">{{__('general.code')}}</th>
|
||||||
|
<th class="py-2">{{__('general.name')}}</th>
|
||||||
|
<th class="py-2">{{__('general.rack')}}</th>
|
||||||
|
<th class="py-2">{{__('general.rows')}}</th>
|
||||||
|
<th class="py-2">{{__('general.columns')}}</th>
|
||||||
|
<th class="py-2">{{__('general.capacity')}}</th>
|
||||||
|
<th class="py-2 text-center" width="100px">{{__('laboratory.table_status')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($boxes as $k=>$box)
|
||||||
|
<tr>
|
||||||
|
<td class="py-1 text-center">{{($k+1) + (($_GET['page'] ?? 1) * 20) - 20}}</td>
|
||||||
|
<td class="text-center py-1">
|
||||||
|
<div class="box-action-buttons" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-primary" title="Edit" data-action="edit" data-box_id="{{$box->id}}" data-toggle="modal" data-target="#modal-box" data-backdrop="static"><i class="typcn typcn-edit"></i></button>
|
||||||
|
<a class="btn btn-sm btn-inverse-info" title="Box Map" href="{{url('box/'.$box->id.'/positions')}}"><i class="mdi mdi-grid"></i></a>
|
||||||
|
@if($box->record_status_id==0)
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-primary" title="Restore" onclick="restore_lab({{$box->id}})"><i class="typcn typcn-plus"></i></button>
|
||||||
|
@else
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-danger" title="Delete" onclick="delete_lab({{$box->id}})"><i class="mdi mdi-close-circle"></i></button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-1">{{$box->code}}</td>
|
||||||
|
<td class="py-1">{{$box->name}}</td>
|
||||||
|
<td class="py-1">{{optional(optional($box->rack)->equipment)->name}} / {{optional($box->rack)->name}}</td>
|
||||||
|
<td class="py-1">{{$box->rows}}</td>
|
||||||
|
<td class="py-1">{{$box->columns}}</td>
|
||||||
|
<td class="py-1">{{$box->capacity}}</td>
|
||||||
|
<td class="py-1 text-center"><i class="mdi {{$box->record_status_id == 1 ? 'mdi-check-circle text-primary' : 'mdi-checkbox-blank-circle-outline'}}"></i></td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-right mt-3">
|
||||||
|
{!! $boxes->links('pagination.custom') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal mt-5 fade modal-primary" id="modal-box">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">{{__('_')}}</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" class="form-control" name="box_id" id="box_id" value="0" />
|
||||||
|
<div class="form-vertical">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.code')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="code" id="code" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.box_name')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="name" id="name" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.rack')}} <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-control select2" name="rack_id" id="rack_id" style="width: 100% !important;">
|
||||||
|
<option></option>
|
||||||
|
@foreach($racks as $rack)
|
||||||
|
<option value="{{$rack->id}}">{{optional($rack->equipment)->name}} / {{$rack->name}}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.rows')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" class="form-control" name="rows" id="rows" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.columns')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" class="form-control" name="columns" id="columns" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.capacity')}}</label>
|
||||||
|
<input type="number" class="form-control" name="capacity" id="capacity" value="" placeholder="auto">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success rounded-25" id="btnSave"><i class="fa fa-floppy-o"></i> <span class="save">{{__('lang.save')}}</span><span class="update" style="display: none">{{__('lang.update')}}</span></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 save_url = "{{url('box.save')}}";
|
||||||
|
var update_url = "{{url('box.update')}}";
|
||||||
|
var delete_url = "{{url('box.delete')}}";
|
||||||
|
var restore_url = "{{url('box.restore')}}";
|
||||||
|
var get_url = "{{url('box.get')}}";
|
||||||
|
let modal_add_title = "Add Box";
|
||||||
|
let modal_edit_title = "Edit Box";
|
||||||
|
</script>
|
||||||
|
<script src="{{url(env("APP_URL").'storage/assets/js/admin/box.js?_').time()}}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
610
resources/views/box_positions.blade.php
Normal file
610
resources/views/box_positions.blade.php
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
<!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> 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>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<li class="nav-item {{ request()->segment(1) == 'sample' ? 'active' : '' }}">
|
<li class="nav-item {{ request()->segment(1) == 'sample' ? 'active' : '' }}">
|
||||||
<a class="nav-link" data-toggle="collapse" href="#samples" aria-expanded="false" aria-controls="laboratory">
|
<a class="nav-link" data-toggle="collapse" href="#samples" aria-expanded="false" aria-controls="laboratory">
|
||||||
<i class="mdi mdi-account-child-circle menu-icon"></i>
|
<i class="mdi mdi-account-child-circle menu-icon"></i>
|
||||||
<span class="menu-title text-uppercase">គ្រប់គ្រង Aliquots</span>
|
<span class="menu-title text-uppercase">{{ __('sidebar.aliquot_management') }}</span>
|
||||||
<i class="menu-arrow"></i>
|
<i class="menu-arrow"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="collapse {{ in_array(request()->segment(1), ['sample']) ? 'show' : '' }}" id="samples">
|
<div class="collapse {{ in_array(request()->segment(1), ['sample']) ? 'show' : '' }}" id="samples">
|
||||||
@@ -51,30 +51,30 @@
|
|||||||
<li class="nav-item {{ in_array(request()->segment(1), ['incident-type']) ? 'active' : '' }}">
|
<li class="nav-item {{ in_array(request()->segment(1), ['incident-type']) ? 'active' : '' }}">
|
||||||
<a class="nav-link" data-toggle="collapse" href="#incident-record" aria-expanded="false" aria-controls="incident-record">
|
<a class="nav-link" data-toggle="collapse" href="#incident-record" aria-expanded="false" aria-controls="incident-record">
|
||||||
<i class="mdi mdi-oil-temperature menu-icon"></i>
|
<i class="mdi mdi-oil-temperature menu-icon"></i>
|
||||||
<span class="menu-title text-uppercase">កត់ត្រាឧប្បត្តិហេតុ</span>
|
<span class="menu-title text-uppercase">{{ __('sidebar.incident_records') }}</span>
|
||||||
<i class="menu-arrow"></i>
|
<i class="menu-arrow"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="collapse {{ in_array(request()->segment(1), ['incident-type']) ? 'show' : '' }}" id="incident-record">
|
<div class="collapse {{ in_array(request()->segment(1), ['incident-type']) ? 'show' : '' }}" id="incident-record">
|
||||||
<ul class="nav flex-column sub-menu">
|
<ul class="nav flex-column sub-menu">
|
||||||
<li class="nav-item"> <a class="nav-link" href="">តារាងឧប្បត្តិហេតុ</a></li>
|
<li class="nav-item"> <a class="nav-link" href="">{{ __('sidebar.incident_table') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='incident-type' ? 'active':''}}" href="{{url('incident-type')}}">ប្រភេទឧប្បត្តិហេតុ</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='incident-type' ? 'active':''}}" href="{{url('incident-type')}}">{{ __('sidebar.incident_type') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item {{ in_array(request()->segment(1), ['building', 'room', 'equipment']) ? 'active' : '' }}">
|
<li class="nav-item {{ in_array(request()->segment(1), ['building', 'room', 'equipment', 'rack', 'box']) ? 'active' : '' }}">
|
||||||
<a class="nav-link" data-toggle="collapse" href="#lab-manager" aria-expanded="false" aria-controls="lab_manager">
|
<a class="nav-link" data-toggle="collapse" href="#lab-manager" aria-expanded="false" aria-controls="lab_manager">
|
||||||
<i class="fa fa-folder-open-o menu-icon"></i>
|
<i class="fa fa-folder-open-o menu-icon"></i>
|
||||||
<span class="menu-title text-uppercase">គ្រប់គ្រង</span>
|
<span class="menu-title text-uppercase">{{ __('sidebar.management') }}</span>
|
||||||
<i class="menu-arrow"></i>
|
<i class="menu-arrow"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="collapse {{ in_array(request()->segment(1), ['building', 'room', 'equipment']) ? 'show' : '' }}" id="lab-manager">
|
<div class="collapse {{ in_array(request()->segment(1), ['building', 'room', 'equipment', 'rack', 'box']) ? 'show' : '' }}" id="lab-manager">
|
||||||
<ul class="nav flex-column sub-menu">
|
<ul class="nav flex-column sub-menu">
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='building' ? 'active':''}}" href="{{url('building')}}">អគារ</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='building' ? 'active':''}}" href="{{url('building')}}">{{ __('sidebar.building') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='room' ? 'active':''}}" href="{{url('room')}}">បន្ទប់សុវត្ថិភាព</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='room' ? 'active':''}}" href="{{url('room')}}">{{ __('sidebar.safe_room') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='equipment' ? 'active':''}}" href="{{url('equipment')}}">ទូររក្សាសំណាក</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='equipment' ? 'active':''}}" href="{{url('equipment')}}">{{ __('sidebar.freezer') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link" href="">ធ្នើ</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='rack' ? 'active':''}}" href="{{url('rack')}}">{{ __('sidebar.rack') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link" href="">ប្រអប់សំណាក</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='box' ? 'active':''}}" href="{{url('box')}}">{{ __('sidebar.sample_box') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -82,14 +82,14 @@
|
|||||||
<li class="nav-item {{ in_array(request()->segment(1), ['concept', 'sample-container', 'pathogen']) ? 'active' : '' }}">
|
<li class="nav-item {{ in_array(request()->segment(1), ['concept', 'sample-container', 'pathogen']) ? 'active' : '' }}">
|
||||||
<a class="nav-link" data-toggle="collapse" href="#catalog" aria-expanded="false" aria-controls="catalog">
|
<a class="nav-link" data-toggle="collapse" href="#catalog" aria-expanded="false" aria-controls="catalog">
|
||||||
<i class="mdi mdi-cards-variant menu-icon"></i>
|
<i class="mdi mdi-cards-variant menu-icon"></i>
|
||||||
<span class="menu-title text-uppercase">បញ្ជីគម្រូ</span>
|
<span class="menu-title text-uppercase">{{ __('sidebar.reference_lists') }}</span>
|
||||||
<i class="menu-arrow"></i>
|
<i class="menu-arrow"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="collapse {{ in_array(request()->segment(1), ['concept', 'sample-container', 'pathogen']) ? 'show' : '' }}" id="catalog">
|
<div class="collapse {{ in_array(request()->segment(1), ['concept', 'sample-container', 'pathogen']) ? 'show' : '' }}" id="catalog">
|
||||||
<ul class="nav flex-column sub-menu">
|
<ul class="nav flex-column sub-menu">
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='concept' ? 'active':''}}" href="{{url('concept')}}">បញ្ជីគម្រូ</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='concept' ? 'active':''}}" href="{{url('concept')}}">{{ __('sidebar.reference_lists') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='pathogen' ? 'active':''}}" href="{{url('pathogen')}}">ប្រភេទមេរោគ/ជាតិពុល</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='pathogen' ? 'active':''}}" href="{{url('pathogen')}}">{{ __('sidebar.pathogen_type') }}</a></li>
|
||||||
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='sample-container' ? 'active':''}}" href="{{url('sample-container')}}">ប្រភេទទីបផ្ទុកសំណាក</a></li>
|
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='sample-container' ? 'active':''}}" href="{{url('sample-container')}}">{{ __('sidebar.sample_container_type') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
156
resources/views/rack.blade.php
Normal file
156
resources/views/rack.blade.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
@include('layout.header')
|
||||||
|
</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="row">
|
||||||
|
<div class="col-md-12 grid-margin stretch-card">
|
||||||
|
<div class="card" style="min-height: 80vh;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title"><i class="mdi mdi-dots-vertical menu-icon"></i> {{__('general.sample_rack')}}</h4>
|
||||||
|
<form method="get" action="{{url('rack/search')}}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-12 col-md-9 col-lg-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control form-control-md rounded-left-25 rounded-right-0" value="{{isset($_GET['kword']) ? $_GET['kword'] : ''}}" name="kword" id="kword" placeholder="{{__('laboratory.search_placeholder')}}">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<select class="form-control select2 rounded-left-0 rounded-right-0" name="equipment_id" style="width: 280px !important;">
|
||||||
|
<option></option>
|
||||||
|
@foreach($equipments as $equipment)
|
||||||
|
<option {{@$_GET['equipment_id']==$equipment->id ? 'selected':''}} value="{{$equipment->id}}">
|
||||||
|
{{optional(optional($equipment->room)->building)->name}} / {{optional($equipment->room)->name}} / {{$equipment->name}}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-md btn-inverse-secondary rounded-right-25 px-3" type="submit"><i class="fa fa-search"></i> {{__('general.btn_search')}}</button>
|
||||||
|
<button class="btn btn-md btn-inverse-success waves-effect rounded-25 px-4 ml-3" type="button" data-action="new" data-toggle="modal" data-target="#modal-rack" data-backdrop="static"><i class="typcn typcn-plus"></i> {{__('general.btn_add_new')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="col-sm-12 p-0 mb-1 mt-3">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gradient-light">
|
||||||
|
<th class="py-2" width="50px">{{__('laboratory.table_no')}}</th>
|
||||||
|
<th class="py-2 text-center" width="100px">{{__('laboratory.table_action')}}</th>
|
||||||
|
<th class="py-2" width="120px">{{__('general.code')}}</th>
|
||||||
|
<th class="py-2">{{__('general.name')}}</th>
|
||||||
|
<th class="py-2">{{__('general.freezer')}}</th>
|
||||||
|
<th class="py-2">{{__('general.description')}}</th>
|
||||||
|
<th class="py-2 text-center" width="100px">{{__('laboratory.table_status')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($racks as $k=>$rack)
|
||||||
|
<tr>
|
||||||
|
<td class="py-1 text-center">{{($k+1) + (($_GET['page'] ?? 1) * 20) - 20}}</td>
|
||||||
|
<td class="text-center py-1">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-primary mr-2" title="Edit" data-action="edit" data-rack_id="{{$rack->id}}" data-toggle="modal" data-target="#modal-rack" data-backdrop="static"><i class="typcn typcn-edit"></i></button>
|
||||||
|
@if($rack->record_status_id==0)
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-primary" title="Restore" onclick="restore_lab({{$rack->id}})"><i class="typcn typcn-plus"></i></button>
|
||||||
|
@else
|
||||||
|
<button type="button" class="btn btn-sm btn-inverse-danger" title="Delete" onclick="delete_lab({{$rack->id}})"><i class="mdi mdi-close-circle"></i></button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-1">{{$rack->code}}</td>
|
||||||
|
<td class="py-1">{{$rack->name}}</td>
|
||||||
|
<td class="py-1">{{optional(optional(optional($rack->equipment)->room)->building)->name}} / {{optional(optional($rack->equipment)->room)->name}} / {{optional($rack->equipment)->name}}</td>
|
||||||
|
<td class="py-1">{{$rack->description}}</td>
|
||||||
|
<td class="py-1 text-center"><i class="mdi {{$rack->record_status_id == 1 ? 'mdi-check-circle text-primary' : 'mdi-checkbox-blank-circle-outline'}}"></i></td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-right mt-3">
|
||||||
|
{!! $racks->links('pagination.custom') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal mt-5 fade modal-primary" id="modal-rack">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">{{__('_')}}</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" class="form-control" name="rack_id" id="rack_id" value="0" />
|
||||||
|
<div class="form-vertical">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.code')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="code" id="code" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.rack_name')}} <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="name" id="name" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.freezer')}} <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-control select2" name="equipment_id" id="equipment_id" style="width: 100% !important;">
|
||||||
|
<option></option>
|
||||||
|
@foreach($equipments as $equipment)
|
||||||
|
<option value="{{$equipment->id}}">{{optional(optional($equipment->room)->building)->name}} / {{optional($equipment->room)->name}} / {{$equipment->name}}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label>{{__('general.description')}}</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-success rounded-25" id="btnSave"><i class="fa fa-floppy-o"></i> <span class="save">{{__('lang.save')}}</span><span class="update" style="display: none">{{__('lang.update')}}</span></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 save_url = "{{url('rack.save')}}";
|
||||||
|
var update_url = "{{url('rack.update')}}";
|
||||||
|
var delete_url = "{{url('rack.delete')}}";
|
||||||
|
var restore_url = "{{url('rack.restore')}}";
|
||||||
|
var get_url = "{{url('rack.get')}}";
|
||||||
|
let modal_add_title = "Add Rack";
|
||||||
|
let modal_edit_title = "Edit Rack";
|
||||||
|
</script>
|
||||||
|
<script src="{{url(env("APP_URL").'storage/assets/js/admin/rack.js?_').time()}}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\AntibioticController;
|
use App\Http\Controllers\AntibioticController;
|
||||||
use App\Http\Controllers\BaseController;
|
use App\Http\Controllers\BaseController;
|
||||||
|
use App\Http\Controllers\BoxController;
|
||||||
use App\Http\Controllers\BuildingController;
|
use App\Http\Controllers\BuildingController;
|
||||||
use App\Http\Controllers\CommentController;
|
use App\Http\Controllers\CommentController;
|
||||||
use App\Http\Controllers\ConceptController;
|
use App\Http\Controllers\ConceptController;
|
||||||
@@ -20,9 +21,11 @@ use App\Http\Controllers\QuantityController;
|
|||||||
use App\Http\Controllers\ReportController;
|
use App\Http\Controllers\ReportController;
|
||||||
use App\Http\Controllers\RoleController;
|
use App\Http\Controllers\RoleController;
|
||||||
use App\Http\Controllers\RoomController;
|
use App\Http\Controllers\RoomController;
|
||||||
|
use App\Http\Controllers\RackController;
|
||||||
use App\Http\Controllers\SampleController;
|
use App\Http\Controllers\SampleController;
|
||||||
use App\Http\Controllers\SampleSourceController;
|
use App\Http\Controllers\SampleSourceController;
|
||||||
use App\Http\Controllers\SampleTypeController;
|
use App\Http\Controllers\SampleTypeController;
|
||||||
|
use App\Http\Controllers\StoragePositionController;
|
||||||
use App\Http\Controllers\TestController;
|
use App\Http\Controllers\TestController;
|
||||||
use App\Http\Controllers\TestGroupController;
|
use App\Http\Controllers\TestGroupController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
@@ -140,6 +143,29 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::post('equipment.delete', [EquipmentController::class, 'delete'])->middleware(VerifyBaseLab::class);
|
Route::post('equipment.delete', [EquipmentController::class, 'delete'])->middleware(VerifyBaseLab::class);
|
||||||
Route::post('equipment.restore', [EquipmentController::class, 'restore'])->middleware(VerifyBaseLab::class);
|
Route::post('equipment.restore', [EquipmentController::class, 'restore'])->middleware(VerifyBaseLab::class);
|
||||||
|
|
||||||
|
Route::get('rack', [RackController::class, 'index'])->name('rack')->middleware(VerifyBaseLab::class);
|
||||||
|
Route::get('rack/search', [RackController::class, 'index'])->name('rack.search')->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('rack.save', [RackController::class, 'save'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('rack.get', [RackController::class, 'get'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('rack.update', [RackController::class, 'update'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('rack.delete', [RackController::class, 'delete'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('rack.restore', [RackController::class, 'restore'])->middleware(VerifyBaseLab::class);
|
||||||
|
|
||||||
|
Route::get('box', [BoxController::class, 'index'])->name('box')->middleware(VerifyBaseLab::class);
|
||||||
|
Route::get('box/search', [BoxController::class, 'index'])->name('box.search')->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('box.save', [BoxController::class, 'save'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('box.get', [BoxController::class, 'get'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('box.update', [BoxController::class, 'update'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('box.delete', [BoxController::class, 'delete'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('box.restore', [BoxController::class, 'restore'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::get('box/{box}/positions', [StoragePositionController::class, 'index'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.generate', [StoragePositionController::class, 'generate'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.aliquot.save', [StoragePositionController::class, 'storeAliquot'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.aliquot.remove', [StoragePositionController::class, 'removeAliquot'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.aliquot.move', [StoragePositionController::class, 'moveAliquot'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.aliquot.history', [StoragePositionController::class, 'movementHistory'])->middleware(VerifyBaseLab::class);
|
||||||
|
Route::post('storage-position.aliquot.use-volume', [StoragePositionController::class, 'useVolume'])->middleware(VerifyBaseLab::class);
|
||||||
|
|
||||||
Route::get('concept', [ConceptController::class, 'index'])->name('concept')->middleware(VerifyBaseLab::class);
|
Route::get('concept', [ConceptController::class, 'index'])->name('concept')->middleware(VerifyBaseLab::class);
|
||||||
Route::get('concept/search', [ConceptController::class, 'index'])->name('concept.search')->middleware(VerifyBaseLab::class);
|
Route::get('concept/search', [ConceptController::class, 'index'])->name('concept.search')->middleware(VerifyBaseLab::class);
|
||||||
Route::post('concept.save', [ConceptController::class, 'save'])->middleware(VerifyBaseLab::class);
|
Route::post('concept.save', [ConceptController::class, 'save'])->middleware(VerifyBaseLab::class);
|
||||||
|
|||||||
4
storage/app/.gitignore
vendored
Normal file
4
storage/app/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!private/
|
||||||
|
!public/
|
||||||
|
!.gitignore
|
||||||
2
storage/app/private/.gitignore
vendored
Normal file
2
storage/app/private/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
2
storage/app/public/.gitignore
vendored
Normal file
2
storage/app/public/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Reference in New Issue
Block a user