412 lines
14 KiB
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.');
|
|
}
|
|
}
|