changes to management

This commit is contained in:
2026-08-17 10:18:11 +07:00
parent cc615a58ca
commit ff6be8ba1e
29 changed files with 2785 additions and 19 deletions

View 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()]);
}
}
}

View 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()]);
}
}
}

View 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(' / ')
);
}
}

View 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
View 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');
}
}

View 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
View 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');
}
}

View File

@@ -36,5 +36,12 @@ class Building extends BaseModel
parent::boot();
}
public function organization(){
return $this->belongsTo(Organization::class, 'organization_id', 'id');
}
public function rooms(){
return $this->hasMany(Room::class, 'building_id', 'id');
}
}

View File

@@ -46,5 +46,8 @@ class Equipment extends BaseModel
return $this->belongsTo(Room::class, 'room_id', 'id');
}
public function racks(){
return $this->hasMany(Rack::class, 'equipment_id', 'id');
}
}

View File

@@ -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'));
// }
public function buildings(){
return $this->hasMany(Building::class, 'organization_id', 'id');
}
}

View 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
View 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
View 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');
}
}

View File

@@ -44,5 +44,8 @@ class Room extends BaseModel
return $this->belongsTo(Concept::class, 'biosafety_level_concept_id', 'id');
}
public function equipments(){
return $this->hasMany(Equipment::class, 'room_id', 'id');
}
}

View File

@@ -92,7 +92,18 @@ return [
'sample_entry_by' => 'Entered 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'

View File

@@ -54,5 +54,18 @@ return [
'create_sample' => 'Create Sample',
'appointment' => 'Appointments',
'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',
];

View File

@@ -89,6 +89,17 @@ return [
'sample_entry_by' => 'បញ្ចូលលទ្ធផលដោយ',
'modify_by' => 'កែប្រែលទ្ធផលដោយ',
'code' => 'លេខកូដ'
'code' => 'លេខកូដ',
'name' => 'ឈ្មោះ',
'rack' => 'ធ្នើ',
'rows' => 'ជួរដេក',
'columns' => 'ជួរឈរ',
'capacity' => 'ចំណុះ',
'box_name' => 'ឈ្មោះប្រអប់',
'rack_name' => 'ឈ្មោះធ្នើ',
'freezer' => 'ទូររក្សាសំណាក',
'description' => 'បរិយាយ',
'sample_box' => 'ប្រអប់សំណាក',
'sample_rack' => 'ធ្នើរក្សាសំណាក'
];

View File

@@ -53,7 +53,20 @@ return [
'payment'=> 'តារាងបង់ប្រាក់',
'sample_management' => 'គ្រប់គ្រងសំណាក',
'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' => 'ប្រភេទទីបផ្ទុកសំណាក'

View 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)
}
},
});
}
})

View 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) }
}
})
}
});
});

View 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)
}
},
});
}
})

View 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>&nbsp;{{__('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>&nbsp;&nbsp;<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>

View 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>&nbsp;&nbsp;Save Aliquot</button>
<button type="button" class="btn btn-secondary rounded-25 ml-2" data-dismiss='modal'>{{__('lang.cancel')}}</button>
</div>
</div>
</div>
</div>
@include('layout.footer')
</div>
</div>
</div>
@include('layout.common_script')
<script>
var generate_positions_url = "{{url('storage-position.generate')}}";
var save_aliquot_url = "{{url('storage-position.aliquot.save')}}";
var remove_aliquot_url = "{{url('storage-position.aliquot.remove')}}";
var move_aliquot_url = "{{url('storage-position.aliquot.move')}}";
var movement_history_url = "{{url('storage-position.aliquot.history')}}";
var use_volume_url = "{{url('storage-position.aliquot.use-volume')}}";
</script>
<script src="{{url(env("APP_URL").'storage/assets/js/admin/box_position.js?_').time()}}"></script>
</body>
</html>

View File

@@ -16,7 +16,7 @@
<li class="nav-item {{ request()->segment(1) == 'sample' ? 'active' : '' }}">
<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>
<span class="menu-title text-uppercase">គ្រប់គ្រង Aliquots</span>
<span class="menu-title text-uppercase">{{ __('sidebar.aliquot_management') }}</span>
<i class="menu-arrow"></i>
</a>
<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' : '' }}">
<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>
<span class="menu-title text-uppercase">កត់ត្រាឧប្បត្តិហេតុ</span>
<span class="menu-title text-uppercase">{{ __('sidebar.incident_records') }}</span>
<i class="menu-arrow"></i>
</a>
<div class="collapse {{ in_array(request()->segment(1), ['incident-type']) ? 'show' : '' }}" id="incident-record">
<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 {{request()->segment(1)=='incident-type' ? 'active':''}}" href="{{url('incident-type')}}">ប្រភេទឧប្បត្តិហេតុ</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')}}">{{ __('sidebar.incident_type') }}</a></li>
</ul>
</div>
</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">
<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>
</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">
<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)=='room' ? 'active':''}}" href="{{url('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" href="">ធ្នើ</a></li>
<li class="nav-item"> <a class="nav-link" href="">ប្រអប់សំណាក</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')}}">{{ __('sidebar.safe_room') }}</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 {{request()->segment(1)=='rack' ? 'active':''}}" href="{{url('rack')}}">{{ __('sidebar.rack') }}</a></li>
<li class="nav-item"> <a class="nav-link {{request()->segment(1)=='box' ? 'active':''}}" href="{{url('box')}}">{{ __('sidebar.sample_box') }}</a></li>
</ul>
</div>
</li>
@@ -82,14 +82,14 @@
<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">
<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>
</a>
<div class="collapse {{ in_array(request()->segment(1), ['concept', 'sample-container', 'pathogen']) ? 'show' : '' }}" id="catalog">
<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)=='pathogen' ? 'active':''}}" href="{{url('pathogen')}}">ប្រភេទមេរោគ/ជាតិពុល</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)=='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')}}">{{ __('sidebar.pathogen_type') }}</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>
</div>
</li>

View 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>&nbsp;{{__('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>&nbsp;&nbsp;<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>

View File

@@ -2,6 +2,7 @@
use App\Http\Controllers\AntibioticController;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\BoxController;
use App\Http\Controllers\BuildingController;
use App\Http\Controllers\CommentController;
use App\Http\Controllers\ConceptController;
@@ -20,9 +21,11 @@ use App\Http\Controllers\QuantityController;
use App\Http\Controllers\ReportController;
use App\Http\Controllers\RoleController;
use App\Http\Controllers\RoomController;
use App\Http\Controllers\RackController;
use App\Http\Controllers\SampleController;
use App\Http\Controllers\SampleSourceController;
use App\Http\Controllers\SampleTypeController;
use App\Http\Controllers\StoragePositionController;
use App\Http\Controllers\TestController;
use App\Http\Controllers\TestGroupController;
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.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/search', [ConceptController::class, 'index'])->name('concept.search')->middleware(VerifyBaseLab::class);
Route::post('concept.save', [ConceptController::class, 'save'])->middleware(VerifyBaseLab::class);

4
storage/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore

2
storage/app/private/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
storage/app/public/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore