Aplikasi Booking Online Laravel 9 & Livewire #3: Livewire Component & Sistem Antrian

Aplikasi Booking Online Laravel 9 & Livewire #3: Livewire Component & Sistem Antrian

Pendahuluan

Fitur Janji Temu atau Appointment telah dibuat, berdasarkan data tersebut, kita akan mengolahnya menjadi antrian online. Data akan dikelompokkan dan diurutkan sesuai urutan pasien yang lebih dahulu membuat appointment. Ada dua buah tampilan informasi yang akan kita buat, pertama adalah informasi tentang nomor antrian yang sedang dilayani dan yang kedua adalah informasi tentang antrian selanjutnya.

Selain itu, melalui artikel ini, kita akan melengkapi informasi singkat tentang berapa jumlah pasien, antrian dan total pasien secara keseluruhan yang telah dilayani oleh klinik terkait. Adapun tampilannya akan terlihat seperti gambar berikut

Baca Juga: Aplikasi Booking Online Laravel 9 & Livewire #2: Blade Templating & Appointment

Livewire Component

Salah satu fitur menarik dari Livewire adalah Component, dimana kita bisa memecah sebuah fitur menjadi module-module kecil dengan tujuan agar lebih mudah dalam maintenance. Misal nya saja, pada gambar di atas, kita akan memecahnya menjadi dua bagian, pertama adalah informasi antrian menjadi sebuah component baru yang kita beri nama Progress dan kedua adalah informasi tentang total pasien menjadi component dengan nama Summary.

Generate component Summary dengan command

php artisan make:livewire Order.Summary

Buka file resource/views/livewire/order/summary.blade.php dan modifikasi menjadi

<div>
    <div class="container">
        <div class="row">
            <div class="col-lg-3 col-md-6">
                <div class="count-box">
                    <i class="fas fa-users"></i>
                    <span data-purecounter-start="0" data-purecounter-end="{{ $totalToday }}" data-purecounter-duration="1" class="purecounter">{{ $totalToday }}</span>
                    <p>Pasien (Hari Ini)</p>
                    <button type="button" wire:click="openModal('Data Pasien (Hari Ini)', 1)" data-bs-toggle="modal" data-bs-target="#modalDataPasien" class="btn btn-primary btn-sm mt-2">Lihat Data</button>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 mt-5 mt-md-0">
                <div class="count-box">
                    <i class="far fa-hospital"></i>
                    <span data-purecounter-start="0" data-purecounter-end="{{ $onQueue }}" data-purecounter-duration="1" class="purecounter">{{ $onQueue }}</span>
                    <p>Antrian (Hari Ini)</p>
                    <button type="button" wire:click="openModal('Data Antrian (Hari Ini)', 2)" data-bs-toggle="modal" data-bs-target="#modalDataPasien" class="btn btn-primary btn-sm mt-2">Lihat Antrian</button>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 mt-5 mt-lg-0">
                <div class="count-box">
                    <i class="fas fa-flask"></i>
                    <span data-purecounter-start="0" data-purecounter-end="{{ $complete }}" data-purecounter-duration="1" class="purecounter">{{ $complete }}</span>
                    <p>Tertangani (Hari Ini)</p>
                    <button type="button" wire:click="openModal('Data Tertangani (Hari Ini)', 3)" data-bs-toggle="modal" data-bs-target="#modalDataPasien" class="btn btn-primary btn-sm mt-2">Lihat Data</button>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 mt-5 mt-lg-0">
                <div class="count-box">
                    <i class="fas fa-award"></i>
                    <span data-purecounter-start="0" data-purecounter-end="{{ $total }}" data-purecounter-duration="1" class="purecounter">{{ $total }}</span>
                    <p>Total Pasien</p>
                    <button type="button" wire:click="openModal('Data Total Pasien', 4)" data-bs-toggle="modal" data-bs-target="#modalDataPasien" class="btn btn-primary btn-sm mt-2">Lihat Data</button>
                </div>
            </div>
        </div>
    </div>

  	<!-- MODAL UNTUK MENAMPILKAN DATA ANTRIAN -->
    <div class="modal fade" id="modalDataPasien" tabindex="-1" role="dialog" aria-labelledby="modalPasien" aria-hidden="true">
        <div class="modal-dialog modal-lg" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="modalPasien">{{ $modalTitle }}</h5>
                    </div>
                    <div class="modal-body">
                        <div class="table responsive">
                            <table class="table table-hover table-bordered table-stripped">
                                <thead>
                                    <tr>
                                        <th>No</th>
                                        <th>Nomor Antrian</th>
                                        <th>Slot Waktu</th>
                                        <th>Status Antrian</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @forelse ($ordersData as $key => $row)
                                    <tr class="{{ $row->status == 1 ? 'bg-warning':'' }}">
                                        <td>{{ $key + 1 }}</td>
                                        <td>{{ $row->order_id }}</td>
                                        <td>{{ $row->daily_slot->name }}</td>
                                        <td>{!! $row->status_label !!}</td>
                                    </tr>
                                    @empty
                                    <tr>
                                        <td colspan="4" class="text-center">Tidak ada pasien</td>
                                    </tr>
                                    @endforelse
                                </tbody>
                            </table>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    </div>
                </div>
            </div>
      </div>
    	<!-- MODAL UNTUK MENAMPILKAN DATA ANTRIAN -->
</div>

Note: Event wire:click akan menjalankan fungsi openModal() dengan mengirimkan 2 parameter. Parameter pertama adalah judul yang akan ditampilkan sebagai judul modal, tujuannya agar user tahu informasi apa yang sedang dibuka. Parameter kedua adalah kategori sebagai tanda untuk memudahkan kita dalam membuat query data nantinya, karena 1 method ini akan digunakan untuk 4 jenis informasi.

Buka file app/Http/Livewire/Order/Summary.php dan modifikasi menjadi

<?php

namespace App\Http\Livewire\Order;

use Livewire\Component;
use Carbon\Carbon;
use App\Models\Order;

class Summary extends Component
{
    public $modalTitle; //PROPERTY INI UNTUK MENAMPUNG JUDUL MODAL
    public $ordersData = []; //PROPERTY INI UNTUK MENAMPUNG DATA YANG AKAN DITAMPILKAN DI MODAL

    public function render()
    {
        //QUERY SUMMARY DATA PASIEN
        $totalToday = Order::where('day', Carbon::now()->format('Y-m-d'))->count(); //QUERY UNTUK MENGAMBIL TOTAL APPOINTMENT HARI INI
        $onQueue = Order::where('day', Carbon::now()->format('Y-m-d'))->whereIn('status', [0, 1])->count(); //QUERY UNTUK MENGAMBIL TOTAL APPOINTMENT HARI INI DENGAN SYARAT MASIH DALAM ANTRIAN (0) / SEDANG DILAYANI (1)
        $complete = Order::where('day', Carbon::now()->format('Y-m-d'))->where('status', 2)->count(); //QUERY UNTUK MENGAMBIL TOTAL APPOINTMENT HARI INI DENGAN SYARAT SUDAH SELESAI (2)
        $total = Order::whereIn('status', [2,3])->count(); //QUERY UNTUK MENGAMBIL TOTAL APPOINTMENT HARI INI DENGAN STATUS SELESAI / DIBATALKAN

        return view('livewire.order.summary', compact('totalToday', 'onQueue', 'complete', 'total'));
    }

    public function openModal($title, $modalType)
    {
        $this->modalTitle = $title; //PARAMETER TITLE YG DIKIRIM KITA SIMPAN KE DALAM PROPERTY INI

        //CODE: 1: TOTAL PASIEN HARI INI, 2: ANTRIAN HARI INI, 3: TERTANGANI HARI INI, 4: TOTAL PASIEN
      
        //JIKA YANG DI MINTA ADALAH TOTAL PASIEN (4)
        if ($modalType == 4) {
            //BUAT QUERY UNTUK MENGAMBIL SEMUA DATA DENGAN STATUS SELESAI (2) & DIBATALKAN (3) DAN DIURUTKAN BERDASARKAN DATA TERBARU
            $ordersData = Order::with(['daily_slot'])->whereIn('status', [2, 3])->orderBy('created_at', 'DESC')->get();
        } else {
          //SELAIN ITU, KITA BUAT QUERY DATA HARI INI
            $ordersData = Order::with(['daily_slot'])->where('day', Carbon::now()->format('Y-m-d'))
                //DENGAN KETENTUAN
                ->when($modalType, function($query) use($modalType) {
                  //JIKA YG DIMINTA ADALAH ANTRIAN HARI INI (2)
                    if ($modalType == 2) {
                        //MAKA FILTER STATUS DALAM ANTRIAN (0) DAN SEDANG DILAYANI (1)
                        $query->whereIn('status', [0, 1]);
                    //JIKA YG DIMINTA ADALAH DATA YG SUDAH SELESAI
                    } elseif ($modalType == 3) {
                      //MAKA FILTER STATUS PASIEN YANG SUDAH DILAYANI (2)
                        $query->where('status', 2);
                    }
                })
                ->orderBy('daily_slot_id', 'ASC') //URUTKAN BERDASARKAN DAILY SLOT, AGAR YANG DITAMPILKAN LEBIH DAHULU ADALAH DATA PAGI HARI, SORE LALU MALAM
                ->orderBy('created_at', 'ASC') //KEMUDIAN URUTKAN BERDASARKAN DATA YANG LEBIH DAHULU MEMBUAT APPOINTMENT
                ->get();
        }
        
        $this->ordersData = $ordersData; //SIMPAN DATA TERSEBUT KE DALAM PROEPRTY TERKAIT
    }
}

Pertanyaannya, bagaimana caranya agar component yang baru saja dibuat bisa kita gunakan? Buka file resources/views/livewire/booking-online.blade.php dan cari tag <section dengan id counts, kemudian kita replace menjadi

<section id="counts" class="counts">
    @livewire('order.summary')
</section>

Query sebelumnya kita menggunakan eager loading untuk me-load relationships dengan nama daily_slot dan juga sebuah accessor dengan nama status_label. Buka file App/Models/Order.php dan tambahkan code berikut

public function getStatusLabelAttribute()
{
    if ($this->status == 0) {
        return '<span class="text-secondary"><small>Dalam Antrian</small></span>';
    } elseif ($this->status == 1) {
        return '<span class="text-primary"><small>Sedang Dilayani</small></span>';
    } elseif ($this->status == 2) {
        return '<span class="text-success"><small>Selesai</small></span>';
    }
    return '<span class="text-danger"><small>Ditangguhkan</small></span>';
}

public function daily_slot()
{
    return $this->belongsTo(DailySlot::class);
}

Masih dengan file yang sama, modifikasi $appends menjadi

protected $appends = ['order_id', 'status_label'];

Summary dari appointment beserta list data-nya berdasarkan 4 kategori sudah selesai, apabila kita jalankan, maka akan terlihat seperti gambar berikut

Sistem Antrian

Module yang akan kita kerjakan kali ini terdiri dari dua informasi, Nomor antrian yang sedang dilayani dan antrian berikutnya. Kedua informasi ini akan kita handle dalam component sendiri.

Generate component baru dengan command

php artisan make:livewire Order.Progress

Buka file resources/views/livewire/order/progress.blade.php dan tambahkan code

<div>
    <div class="container mb-5">
        <div class="row">
            <div class="col-md-6">
                <div class="count-box bg-success">
                    <i class="fas fa-stethoscope"></i>
                    <h4 class="text-white">Sedang Dilayani</h4>
                    <hr>

                    @foreach ($onProgress as $row)
                    <h1 class="text-white">{{ $row->order_id }}</h1>
                    @endforeach
                </div>
            </div>
            <div class="col-md-6">
                <div class="count-box" style="background: #AABEC6">
                    <i class="fas fa-user-md"></i>
                    <h4>Antrian Selanjutnya</h4>
                    <hr>

                    @foreach ($next as $val)
                    <h5>{{ $val->order_id }}</h5>
                    @endforeach
                </div>
            </div>
        </div>
    </div>
</div>

Selanjutnya, buka file app/Http/Livewire/Order/Progress.php dan modifikasi menjadi

<?php

namespace App\Http\Livewire\Order;

use Livewire\Component;
use Carbon\Carbon;
use App\Models\Order;

class Progress extends Component
{
    public function render()
    {
        //QUERY UNTUK MENGAMBIL DATA APPOINTMENT HARI INI DENGAN STATUS SEDANG DILAYANI (1), DATANYA DI LIMIT 2 DATA
        $onProgress = Order::where('status', 1)->where('day', Carbon::now()->format('Y-m-d'))->take(2)->get();
        //MEMBUAT QUERY DATA APPOINTMENT HARI INI YANG DIURUTKAN BERDASARKAN DAILY SLOT PAGI, SORE, MALAM DAN DIURUTKAN LAGI BERDASRKAN PASIEN YANG MENDAFTARKAN LEBIH DULU
      //DATA DILIMIT 3 ITEM
        $next = Order::where('status', 0)
            ->where('day', Carbon::now()->format('Y-m-d'))
            ->orderBy('daily_slot_id', 'ASC')
            ->orderBy('created_at', 'ASC')
            ->take(3)->get();
        return view('livewire.order.progress', compact('onProgress', 'next'));
    }
}

Component yang baru saja dibuat akan kita panggil di dalam file blade component Summary agar tampilannya menjadi 1 bagian. Buka file resources/views/livewire/order/summary.blade.php dan tambahkan code dibawah ini tepat di atas tag <div class="container">

@livewire('order.progress')

 

Fixing Bugs

Saat proses development, saya menemukan bug saat kita memilih slot waktu ketika membuat appointment, angka pada kuota yang telah terisi menjadi hilang. Hal ini disebabkan karena Livewire akan me-render ulang component-nya ketika terjadi perubahan data dan data dari fungsi withCount() hilang.

Untuk mengatasi hal ini, kita akan menggunakan Lifecycle Hooks dari Livewire dengan nama hydrate(). Buka file app/Http/Livewire/Order/Appointment.php dan tambahkan method

public function hydrate()
{
    //JIKA PROPERTY timeSlot ADALAH SEBUAH COLLECTION
    if (is_a($this->timeSlot, 'Illuminate\Database\Eloquent\Collection')) {
        //MAKA KITA GUNAKAN FUNGSI loadCount UNTUK MENGAMBIL ULANG DATA ORDERS
        $this->timeSlot->loadCount(['orders' => function($query) {
            $query->where('day', $this->day);
        }]);
    }
}

Penyesuaian berikutnya adalah Timezone, karena secara default Laravel menggunakan UTC, maka buka file config/app.php dan modifikasi key timezone menjadi

'timezone' => 'Asia/Jakarta',

Kesimpulan

Module atau Fitur dari layanan yang bisa diakses oleh User atau Pasien telah selesai, sehingga pada artikel berikutnya kita akan membuat admin page untuk mengelola data Appointment & antrian.

Sepanjang artikel ini, kita telah belajar beberapa hal, diataranya

  1. Membuat Livewire component & menggunakan component tersebut.
  2. Menggunakan Lifecycle Hooks.
  3. Membuat modal & menampilkan data secara dinamis berdasarkan data yang diminta.
  4. Membuat model relationships
  5. Menggunakan Eager Loading

Adapun source code dari materi ini bisa kamu download di Github.

Category:
Share:

Comments