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