From f7ccea75dc5b05ea734423f75b6e810a92675c04 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Sat, 21 Feb 2026 05:28:47 +0700 Subject: [PATCH] update --- .../Controllers/BiorepositoryController.php | 100 ++++-- ...00004_alter_bio_tables_for_slot_system.php | 98 ++++++ .../views/admin/biorepository.blade.php | 304 ++++++++++++------ 3 files changed, 387 insertions(+), 115 deletions(-) create mode 100644 htdocs/database/migrations/2026_02_20_000004_alter_bio_tables_for_slot_system.php diff --git a/htdocs/app/Http/Controllers/BiorepositoryController.php b/htdocs/app/Http/Controllers/BiorepositoryController.php index 27f5e714..0ff96f3a 100644 --- a/htdocs/app/Http/Controllers/BiorepositoryController.php +++ b/htdocs/app/Http/Controllers/BiorepositoryController.php @@ -20,14 +20,13 @@ class BiorepositoryController extends Controller $data = []; $data['cabinetOptions'] = BioCabinet::orderBy('name', 'ASC')->get(); - $data['rackOptions'] = BioRack::with('cabinet')->orderBy('name', 'ASC')->get(); $data['totalCabinets'] = BioCabinet::count(); $data['totalRacks'] = BioRack::count(); $data['totalSpecimens'] = BioSpecimen::count(); $data['cabinets'] = BioCabinet::with([ 'racks' => function ($query) { - $query->orderBy('level', 'ASC')->orderBy('name', 'ASC'); + $query->orderBy('level', 'ASC')->orderBy('rack_number', 'ASC'); }, 'racks.specimens' => function ($query) { $query->orderBy('stored_at', 'ASC'); @@ -77,7 +76,9 @@ class BiorepositoryController extends Controller 'code' => 'required|max:50', 'name' => 'required|max:150', 'level' => 'required|integer|min:1', - 'capacity' => 'nullable|integer|min:0', + 'rack_number' => 'required|integer|min:1', + 'box_number' => 'required|integer|min:1', + 'capacity' => 'required|integer|min:1', 'notes' => 'nullable', ]); @@ -98,7 +99,9 @@ class BiorepositoryController extends Controller 'code' => $request->input('code'), 'name' => $request->input('name'), 'level' => $request->input('level'), - 'capacity' => $request->input('capacity') ?? 0, + 'rack_number' => $request->input('rack_number'), + 'box_number' => $request->input('box_number'), + 'capacity' => $request->input('capacity'), 'notes' => $request->input('notes'), ]); @@ -109,14 +112,15 @@ class BiorepositoryController extends Controller { $validator = Validator::make($request->all(), [ 'rack_id' => 'required|exists:bio_racks,id', - 'specimen_code' => 'required|max:100|unique:bio_specimens,specimen_code', - 'specimen_name' => 'required|max:200', - 'patient_name' => 'nullable|max:200', - 'collected_at' => 'nullable|date', + 'kategorisimpan' => 'required|in:A,B,C,D', + 'shelfnomor' => 'required|integer|min:1', + 'raknomor' => 'required|integer|min:1', + 'slotnomor' => 'required|integer|min:1', + 'boxnomor' => 'required|integer|min:1', + 'tubenomor' => 'required|integer|min:1', + 'nmbakteri' => 'required|max:200', + 'strain' => 'required|in:Gram Negatif,Gram Positif', 'stored_at' => 'required|date', - 'volume' => 'nullable|max:50', - 'storage_condition' => 'nullable|max:100', - 'notes' => 'nullable', ]); if ($validator->fails()) { @@ -124,20 +128,80 @@ class BiorepositoryController extends Controller } $rack = BioRack::find($request->input('rack_id')); + $expectedRackNo = $rack->rack_number ?: $rack->id; + $expectedBoxNo = $rack->box_number ?: 1; + + if ((int) $request->input('shelfnomor') !== (int) $rack->level || + (int) $request->input('raknomor') !== (int) $expectedRackNo || + (int) $request->input('boxnomor') !== (int) $expectedBoxNo) { + return back()->withErrors(['slotnomor' => 'Posisi slot tidak sesuai dengan konfigurasi rack.'])->withInput(); + } + + if ((int) $request->input('slotnomor') > (int) $rack->capacity) { + return back()->withErrors(['slotnomor' => 'Slot melebihi kapasitas rack.'])->withInput(); + } + + $slotTaken = BioSpecimen::where('rack_id', $rack->id) + ->where('slot_number', $request->input('slotnomor')) + ->exists(); + + if ($slotTaken) { + return back()->withErrors(['slotnomor' => 'Slot ini sudah terisi spesimen.'])->withInput(); + } + + $atccByUser = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', Session::get('nama', 'UNKNOWN'))); + if ($atccByUser === '') { + $atccByUser = 'UNKNOWN'; + } + + $generatedCode = implode('-', [ + $request->input('kategorisimpan'), + $request->input('shelfnomor'), + $request->input('raknomor'), + $request->input('slotnomor'), + $request->input('boxnomor'), + $atccByUser, + ]); + + $baseCode = $generatedCode; + $counter = 1; + while (BioSpecimen::where('specimen_code', $generatedCode)->exists()) { + $counter++; + $generatedCode = $baseCode.'-'.$counter; + } BioSpecimen::create([ 'cabinet_id' => $rack->cabinet_id, 'rack_id' => $rack->id, - 'specimen_code' => $request->input('specimen_code'), - 'specimen_name' => $request->input('specimen_name'), - 'patient_name' => $request->input('patient_name'), - 'collected_at' => $request->input('collected_at'), + 'specimen_code' => $generatedCode, + 'specimen_name' => $request->input('nmbakteri'), + 'category_storage' => $request->input('kategorisimpan'), + 'shelf_number' => $request->input('shelfnomor'), + 'rack_number' => $request->input('raknomor'), + 'slot_number' => $request->input('slotnomor'), + 'box_number' => $request->input('boxnomor'), + 'tube_number' => $request->input('tubenomor'), + 'bacteria_name' => $request->input('nmbakteri'), + 'strain' => $request->input('strain'), + 'atcc' => $atccByUser, + 'input_by' => Session::get('nama'), 'stored_at' => $request->input('stored_at'), - 'volume' => $request->input('volume'), - 'storage_condition' => $request->input('storage_condition'), + 'storage_condition' => $this->mapStorageCondition($request->input('kategorisimpan')), 'notes' => $request->input('notes'), ]); - return redirect('/biorepository')->with('success', 'Spesimen berhasil ditambahkan.'); + return redirect('/biorepository')->with('success', 'Spesimen berhasil disimpan ke slot terpilih.'); + } + + private function mapStorageCondition($category) + { + $mapping = [ + 'A' => 'Suhu Ruang', + 'B' => '4 Derajat', + 'C' => '20 Derajat', + 'D' => '80 Derajat', + ]; + + return $mapping[$category] ?? 'Tidak Diketahui'; } } diff --git a/htdocs/database/migrations/2026_02_20_000004_alter_bio_tables_for_slot_system.php b/htdocs/database/migrations/2026_02_20_000004_alter_bio_tables_for_slot_system.php new file mode 100644 index 00000000..8e7a6244 --- /dev/null +++ b/htdocs/database/migrations/2026_02_20_000004_alter_bio_tables_for_slot_system.php @@ -0,0 +1,98 @@ +integer('rack_number')->nullable()->after('level'); + } + if (!Schema::hasColumn('bio_racks', 'box_number')) { + $table->integer('box_number')->nullable()->after('rack_number'); + } + }); + + Schema::table('bio_specimens', function (Blueprint $table) { + if (!Schema::hasColumn('bio_specimens', 'category_storage')) { + $table->string('category_storage', 1)->nullable()->after('specimen_name'); + } + if (!Schema::hasColumn('bio_specimens', 'shelf_number')) { + $table->integer('shelf_number')->nullable()->after('category_storage'); + } + if (!Schema::hasColumn('bio_specimens', 'rack_number')) { + $table->integer('rack_number')->nullable()->after('shelf_number'); + } + if (!Schema::hasColumn('bio_specimens', 'slot_number')) { + $table->integer('slot_number')->nullable()->after('rack_number'); + } + if (!Schema::hasColumn('bio_specimens', 'box_number')) { + $table->integer('box_number')->nullable()->after('slot_number'); + } + if (!Schema::hasColumn('bio_specimens', 'tube_number')) { + $table->integer('tube_number')->nullable()->after('box_number'); + } + if (!Schema::hasColumn('bio_specimens', 'bacteria_name')) { + $table->string('bacteria_name', 200)->nullable()->after('tube_number'); + } + if (!Schema::hasColumn('bio_specimens', 'strain')) { + $table->string('strain', 50)->nullable()->after('bacteria_name'); + } + if (!Schema::hasColumn('bio_specimens', 'atcc')) { + $table->string('atcc', 150)->nullable()->after('strain'); + } + if (!Schema::hasColumn('bio_specimens', 'input_by')) { + $table->string('input_by', 150)->nullable()->after('atcc'); + } + }); + } + + public function down(): void + { + Schema::table('bio_racks', function (Blueprint $table) { + if (Schema::hasColumn('bio_racks', 'rack_number')) { + $table->dropColumn('rack_number'); + } + if (Schema::hasColumn('bio_racks', 'box_number')) { + $table->dropColumn('box_number'); + } + }); + + Schema::table('bio_specimens', function (Blueprint $table) { + if (Schema::hasColumn('bio_specimens', 'category_storage')) { + $table->dropColumn('category_storage'); + } + if (Schema::hasColumn('bio_specimens', 'shelf_number')) { + $table->dropColumn('shelf_number'); + } + if (Schema::hasColumn('bio_specimens', 'rack_number')) { + $table->dropColumn('rack_number'); + } + if (Schema::hasColumn('bio_specimens', 'slot_number')) { + $table->dropColumn('slot_number'); + } + if (Schema::hasColumn('bio_specimens', 'box_number')) { + $table->dropColumn('box_number'); + } + if (Schema::hasColumn('bio_specimens', 'tube_number')) { + $table->dropColumn('tube_number'); + } + if (Schema::hasColumn('bio_specimens', 'bacteria_name')) { + $table->dropColumn('bacteria_name'); + } + if (Schema::hasColumn('bio_specimens', 'strain')) { + $table->dropColumn('strain'); + } + if (Schema::hasColumn('bio_specimens', 'atcc')) { + $table->dropColumn('atcc'); + } + if (Schema::hasColumn('bio_specimens', 'input_by')) { + $table->dropColumn('input_by'); + } + }); + } +}; diff --git a/htdocs/resources/views/admin/biorepository.blade.php b/htdocs/resources/views/admin/biorepository.blade.php index c0bba1ee..49822b48 100644 --- a/htdocs/resources/views/admin/biorepository.blade.php +++ b/htdocs/resources/views/admin/biorepository.blade.php @@ -14,7 +14,7 @@ } .rack-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; } .rack-box { @@ -22,21 +22,35 @@ border-radius: 10px; background: #ffffff; padding: 12px; - min-height: 180px; } .rack-header { font-weight: 700; color: #1b3a57; - margin-bottom: 6px; - } - .specimen-preview { - margin: 0; - padding-left: 15px; - font-size: 12px; - } - .specimen-preview li { margin-bottom: 4px; } + .slot-grid { + display: grid; + grid-template-columns: repeat(8, minmax(26px, 1fr)); + gap: 6px; + margin-top: 10px; + } + .slot-btn { + border: 1px solid #bfd3e8; + background: #f4f9ff; + color: #134068; + border-radius: 5px; + font-size: 11px; + line-height: 1; + padding: 7px 0; + cursor: pointer; + text-align: center; + } + .slot-btn.slot-filled { + background: #fce8e8; + border-color: #e7adad; + color: #8d1f1f; + cursor: not-allowed; + } .oldest-highlight { border: 1px solid #ffd58f; background: #fff8ea; @@ -104,7 +118,7 @@
-
+
Tambah Lemari

@@ -131,7 +145,7 @@
-
+
Tambah Rack

@@ -146,80 +160,38 @@ @endforeach
-
- - -
-
- - +
+
+ + +
+
+ + +
-
- +
+
-
- - +
+ + +
+
+ + +
+
+ +
- -
-
-
Tambah Spesimen
-

-
- @csrf -
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
-
-
@@ -228,7 +200,7 @@

Spesimen dengan Waktu Simpan Paling Lama

@if($oldestSpecimen)
- {{ $oldestSpecimen->specimen_code }} - {{ $oldestSpecimen->specimen_name }}
+ {{ $oldestSpecimen->specimen_code }} - {{ $oldestSpecimen->bacteria_name ?? $oldestSpecimen->specimen_name }}
Lemari: {{ $oldestSpecimen->cabinet->name ?? '-' }} | Rack: {{ $oldestSpecimen->rack->name ?? '-' }}
Disimpan sejak: {{ $oldestSpecimen->stored_at }} ({{ $oldestStorageDays }} hari)
@@ -242,35 +214,48 @@
-

Visualisasi Lemari dan Rack

+

Visualisasi Lemari, Rack, dan Slot

+

Klik slot berwarna biru untuk mengisi spesimen. Slot merah artinya sudah terisi.

@forelse($cabinets as $cabinet)
{{ $cabinet->code }} - {{ $cabinet->name }} ({{ $cabinet->location ?? 'Lokasi belum diisi' }})
@forelse($cabinet->racks as $rack) + @php + $slotMap = []; + foreach ($rack->specimens as $item) { + if ($item->slot_number) { + $slotMap[$item->slot_number] = $item; + } + } + $capacity = (int) $rack->capacity; + @endphp
-
{{ $rack->code }} - {{ $rack->name }} (Lv. {{ $rack->level }})
-
Kapasitas: {{ $rack->capacity }}
-
Total spesimen: {{ $rack->specimens->count() }}
+
{{ $rack->code }} - {{ $rack->name }}
+
Shelf {{ $rack->level }} | Rack {{ $rack->rack_number ?? $rack->id }} | Box {{ $rack->box_number ?? 1 }}
+
Slot: {{ $capacity }} | Terisi: {{ $rack->specimens->count() }}
- @if($rack->specimens->count() > 0) - @php - $oldestRackSpecimen = $rack->specimens->first(); - $preview = $rack->specimens->take(4); - @endphp -
Terlama di rack ini: {{ $oldestRackSpecimen->specimen_code }} ({{ $oldestRackSpecimen->storage_days }} hari)
-
Preview spesimen:
-
    - @foreach($preview as $sp) -
  • - {{ $sp->specimen_code }} - {{ $sp->specimen_name }}
    - Simpan {{ $sp->stored_at }} ({{ $sp->storage_days }} hari) -
  • - @endforeach -
+ @if($capacity > 0) +
+ @for($i = 1; $i <= $capacity; $i++) + @if(isset($slotMap[$i])) +
{{ $i }}
+ @else + + @endif + @endfor +
@else -
Belum ada spesimen pada rack ini.
+
Kapasitas slot belum diatur.
@endif
@empty @@ -286,4 +271,129 @@
+ + @endsection + +@push('script') + +@endpush