si-klinik/sk-klinik.my.id/app/Livewire/Admin/ManageAntrean.php

412 lines
14 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\Appointment;
use App\Models\Doctor;
use App\Models\Patient;
use App\Models\ServiceCategory;
use App\Models\User;
use App\Models\MedicalRecord;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Livewire\WithPagination;
use Masmerise\Toaster\Toaster;
class ManageAntrean extends Component
{
use WithPagination;
// Filter & State Properti
public $search_patient_name = '';
public $search_date;
public $filter_doctor = '';
public $target_number;
public $current_tab = 'active'; // active (waiting, calling, skipped), finished, cancelled
public $selectedAppointment = null;
// Properti Form Walk-in
public $walkin_patient_id;
public $walkin_doctor_id;
public $walkin_service_id;
public $walkin_notes;
// Properti untuk Rekam Medis
public $currentExamining = null;
public $symptoms;
public $diagnosis;
public $treatment;
public $doctor_notes;
protected $queryString = [
'search_date' => ['except' => ''],
'current_tab' => ['except' => 'active'],
'filter_doctor' => ['except' => '']
];
public function mount()
{
// Default ke tanggal hari ini jika tidak ada di URL
$this->search_date = $this->search_date ?? date('Y-m-d');
}
public function updatedSearchDate()
{
$this->resetPage();
}
public function updatedFilterDoctor()
{
$this->resetPage();
}
public function updatedCurrentTab()
{
$this->resetPage();
}
/**
* Fungsi Utama Update Status & Trigger Suara
*/
public function updateStatus($id, $status)
{
$appointment = Appointment::with(['patient', 'doctor', 'serviceCategory'])->findOrFail($id);
// LOGIKA KHUSUS JIKA STATUS 'CALLING'
if ($status === 'calling') {
// Matikan pasien lain yang sedang 'calling' di dokter yang sama agar tidak bentrok
Appointment::where('doctor_id', $appointment->doctor_id)
->whereDate('appointment_date', $this->search_date)
->where('status', 'calling')
->where('id', '!=', $id)
->update(['status' => 'waiting']);
// Kirim event suara ke Browser
$this->dispatch(
'play-calling-sound',
name: $appointment->patient->name,
number: $appointment->queue_number,
poli: $appointment->serviceCategory->name ?? 'Ruang Pemeriksaan',
doctor: $appointment->doctor->name ?? ''
);
}
$appointment->update(['status' => $status]);
$message = match ($status) {
'calling' => 'Pasien dipanggil ke ruang periksa.',
'finished' => 'Pemeriksaan telah selesai.',
'cancelled' => 'Antrean berhasil dibatalkan.',
'skipped' => 'Pasien ditandai terlambat.',
default => 'Status berhasil diperbarui.'
};
Toaster::success($message);
}
/**
* Panggil Ulang Pasien (Hanya trigger suara)
*/
public function recall($id)
{
$appointment = Appointment::with(['patient', 'doctor', 'serviceCategory'])->findOrFail($id);
$this->dispatch(
'play-calling-sound',
name: $appointment->patient->name,
number: $appointment->queue_number,
poli: $appointment->serviceCategory->name ?? 'Ruang Pemeriksaan',
doctor: $appointment->doctor->name ?? ''
);
Toaster::info("Memanggil ulang antrean nomor #{$appointment->queue_number}");
}
/**
* Tandai Dilewati dan Langsung Panggil Berikutnya
*/
public function skipAndNext($id)
{
$appointment = Appointment::findOrFail($id);
$appointment->update(['status' => 'skipped']);
Toaster::warning("Pasien #{$appointment->queue_number} ditandai dilewati.");
$this->callNext();
}
/**
* Kembalikan Status (Undo)
*/
public function undoStatus($id)
{
$appointment = Appointment::findOrFail($id);
$appointment->update(['status' => 'waiting']);
Toaster::info("Antrean #{$appointment->queue_number} dikembalikan ke status menunggu.");
}
/**
* Panggil Pasien Berikutnya secara Otomatis
*/
public function callNext()
{
$query = Appointment::where('status', 'waiting')
->whereDate('appointment_date', $this->search_date);
// Jika sedang filter dokter tertentu
if (!empty($this->filter_doctor)) {
$next = $query->where('doctor_id', $this->filter_doctor)
->orderBy('queue_number', 'asc')
->first();
}
// Jika mode Global (Semua Dokter)
else {
$next = $query->orderBy('created_at', 'asc')->first();
}
if ($next) {
// Sinkronkan filter ke dokter pasien tersebut jika di mode global
if (empty($this->filter_doctor)) {
$this->filter_doctor = $next->doctor_id;
}
$this->updateStatus($next->id, 'calling');
Toaster::success("Memanggil berikutnya: #{$next->queue_number}");
} else {
Toaster::error('Tidak ada pasien dalam antrean menunggu.');
}
}
/**
* Panggil Berdasarkan Input Nomor Manual
*/
public function callSpecificNumber()
{
if (empty($this->target_number)) {
Toaster::error('Masukkan nomor antrean.');
return;
}
$query = Appointment::where('queue_number', $this->target_number)
->whereDate('appointment_date', $this->search_date)
->whereIn('status', ['waiting', 'skipped']);
if (!empty($this->filter_doctor)) {
$query->where('doctor_id', $this->filter_doctor);
}
$results = $query->get();
if ($results->count() === 0) {
Toaster::error("Nomor #{$this->target_number} tidak ditemukan.");
} elseif ($results->count() > 1 && empty($this->filter_doctor)) {
Toaster::warning("Ada beberapa nomor sama. Pilih dokter dahulu.");
} else {
$target = $results->first();
$this->filter_doctor = $target->doctor_id;
$this->updateStatus($target->id, 'calling');
$this->target_number = '';
}
}
/**
* Pindahkan Pasien ke Dokter/Poli Lain
*/
public function transferPatient($id, $newDoctorId)
{
$appointment = Appointment::findOrFail($id);
// Cari nomor terakhir di dokter tujuan hari ini
$lastQueue = Appointment::where('doctor_id', $newDoctorId)
->whereDate('appointment_date', $this->search_date)
->max('queue_number') ?? 0;
$appointment->update([
'doctor_id' => $newDoctorId,
'queue_number' => $lastQueue + 1,
'status' => 'waiting'
]);
Toaster::success("Pasien berhasil dipindahkan ke Dokter/Poli tujuan.");
}
/**
* Modal Tiket
*/
public function showTicketModal($id)
{
$this->selectedAppointment = Appointment::with(['patient', 'doctor', 'serviceCategory'])->findOrFail($id);
$this->js("Flux.modal('ticket-modal').show()");
}
/**
* Walk-in Registration
*/
public function openWalkInModal()
{
$this->reset(['walkin_patient_id', 'walkin_doctor_id', 'walkin_service_id', 'walkin_notes']);
if ($this->filter_doctor) $this->walkin_doctor_id = $this->filter_doctor;
$this->js("Flux.modal('walkin-modal').show()");
}
protected function validationAttributes()
{
return [
'walkin_patient_id' => 'pasien',
'walkin_doctor_id' => 'dokter',
'walkin_service_id' => 'poli/layanan',
];
}
public function saveWalkIn()
{
// 1. Validasi pastikan ID tersebut ada di tabel patients
$this->validate([
'walkin_patient_id' => 'required|exists:patients,id',
'walkin_doctor_id' => 'required|exists:doctors,id',
'walkin_service_id' => 'required|exists:service_categories,id',
]);
// 2. AMBIL DATA PASIEN UNTUK MENDAPATKAN USER_ID NYA
$patientData = \App\Models\Patient::with('user')->find($this->walkin_patient_id);
if (!$patientData || !$patientData->user_id) {
Toaster::error('Gagal memproses data pengguna pasien.');
return;
}
$lastQueue = Appointment::where('doctor_id', $this->walkin_doctor_id)
->whereDate('appointment_date', $this->search_date)
->max('queue_number') ?? 0;
// 3. Simpan appointment menggunakan USER_ID (karena database Anda mendefinisikan patient_id sebagai ID User)
$appointment = Appointment::create([
'patient_id' => $patientData->user_id, // Gunakan user_id dari tabel patients
'doctor_id' => $this->walkin_doctor_id,
'service_category_id' => $this->walkin_service_id,
'appointment_date' => $this->search_date,
'queue_number' => $lastQueue + 1,
'notes' => $this->walkin_notes,
'status' => 'waiting',
'created_by' => auth()->id(),
]);
// Ambil data lengkap termasuk relasi untuk dikirim ke JS cetak
$this->dispatch('print-after-save', data: [
'number' => str_pad($appointment->queue_number, 2, '0', STR_PAD_LEFT),
'name' => $patientData->user->name ?? 'Tanpa Nama',
'poli' => $appointment->serviceCategory->name ?? 'UMUM',
'doctor' => $appointment->doctor->name ?? '-',
'date' => $appointment->created_at->translatedFormat('d M Y, H:i') . ' WIB',
]);
$this->js("Flux.modal('walkin-modal').close()");
Toaster::success('Pendaftaran Walk-in berhasil.');
$this->reset(['walkin_patient_id', 'walkin_notes']);
}
public function render()
{
// Query Dasar berdasarkan Tanggal dan Filter Dokter
$baseQuery = Appointment::with(['patient', 'doctor', 'serviceCategory'])
->whereDate('appointment_date', $this->search_date)
->when($this->filter_doctor, function ($q) {
return $q->where('doctor_id', $this->filter_doctor);
});
// Filter Tabel berdasarkan Tab Aktif
$tableQuery = (clone $baseQuery);
if ($this->current_tab === 'active') {
$tableQuery->whereIn('status', ['waiting', 'calling', 'skipped']);
} else {
$tableQuery->where('status', $this->current_tab);
}
return view('livewire.admin.manage-antrean', [
'appointments' => $tableQuery->orderBy('queue_number', 'asc')->paginate(10),
'currentCalling' => (clone $baseQuery)->where('status', 'calling')->first(),
'stats' => [
'total' => (clone $baseQuery)->count(),
'waiting' => (clone $baseQuery)->whereIn('status', ['waiting', 'skipped'])->count(),
'finished' => (clone $baseQuery)->where('status', 'finished')->count(),
],
'doctors' => Doctor::all(),
'services' => ServiceCategory::all(),
'patients' => strlen($this->search_patient_name) >= 3
? User::query()
->has('patient') // Hanya user yang sudah punya data di tabel patients
->with('patient')
->where(function ($q) {
$q->where('name', 'like', '%' . $this->search_patient_name . '%')
->orWhereHas('patient', function ($query) {
$query->where('nik', 'like', '%' . $this->search_patient_name . '%');
});
})
->limit(10)
->get()
: [],
]);
}
public function startMedicalRecord($appointmentId)
{
// 1. Ambil data janji temu beserta relasinya
$this->currentExamining = Appointment::with(['doctor', 'serviceCategory'])->find($appointmentId);
if ($this->currentExamining) {
// 2. Karena 'patient_id' di appointments adalah ID USER, kita langsung cari nama user-nya di sini
$userPasien = \App\Models\User::find($this->currentExamining->patient_id);
// 3. Kita simpan nama aslinya ke dalam properti baru agar bisa dipanggil dengan aman di Blade
$this->currentPatientName = $userPasien ? $userPasien->name : 'Tidak Diketahui';
// Set form default
$this->symptoms = $this->currentExamining->notes;
$this->diagnosis = '';
$this->treatment = '';
$this->doctor_notes = '';
// Munculkan Modal
$this->js("Flux.modal('medical-record-modal').show()");
}
}
public function saveMedicalRecord()
{
$this->validate([
'symptoms' => 'required',
'diagnosis' => 'required',
]);
$actualPatient = \App\Models\Patient::where('user_id', $this->currentExamining->patient_id)->first();
if (! $actualPatient) {
session()->flash('error', 'Data Pasien tidak ditemukan di sistem.');
return;
}
// Simpan ke database dengan ID Patient yang benar
MedicalRecord::create([
'patient_id' => $actualPatient->id, // Sekarang menggunakan ID Patient asli (Bukan 52 lagi)
'appointment_id' => $this->currentExamining->id,
'doctor_id' => $this->currentExamining->doctor_id,
'symptoms' => $this->symptoms,
'diagnosis' => $this->diagnosis,
'treatment' => $this->treatment,
'notes' => $this->doctor_notes,
]);
// Update status antrean menjadi Selesai (finished)
$this->currentExamining->update([
'status' => 'finished'
]);
$this->js("Flux.modal('medical-record-modal').close()");
$this->reset(['currentExamining', 'symptoms', 'diagnosis', 'treatment', 'doctor_notes']);
session()->flash('message', 'Pemeriksaan selesai dan Rekam Medis berhasil disimpan.');
}
}