Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #7: Push Notification Expenses

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #7: Push Notification Expenses

Pendahuluan

Dalam setiap kegiatan berwirausaha pasti terdapat biaya biaya yang harus dikeluarkan untuk menjalankan operasional dari usaha tersebut, maka dalam aplikasi ini, kita tidak hanya akan mencatat transaksi yang masuk tapi juga memiliki fitur untuk mencatat pengeluaran yang telah dikeluarkan.

Adapun schema yang diinginkan adalah setiap user dapat melakukan request untuk setiap keperluan / biaya yang dibutuhkannya dan finance memiliki peran untuk menyetujui/menolak permintaan tersebut disertai dengan alasan sebagai feedback kepada user yang melakukan permintaan.

Pada fitur ini, selain berinteraksi dengan proses manipulasi database, juga kita akan belajar hal baru yakni push notifications secara real time. Jadi ketika user melakukan permintaan, maka secara otomatis notifikasi tersebut akan tampil pada user terkait tanpa harus melakukan reload browser.

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #6: Role & User Permissions

Manage Request Lists

Module ini akan membahas bagaimana membuat fitur untuk menampilkan data request yang telah dilakukan, dimana setiap kurir/admin hanya akan melihat data request-nya sendiri terkecuali finance dan superadmin. Sebelum melangkah lebih jauh, maka kita perlu menambahkan dua buah field kedalam table expanses, pada command line jalankan command:

php artisan make:migration add_field_status_to_expenses_table

Buka file migration tersebut dan modifikasi menjadi:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddFieldStatusToExpensesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('expenses', function (Blueprint $table) {
            $table->char('status', 1)->comment('0:open, 1: approved, 2: canceled')->after('user_id')->nullable();
            $table->string('reason')->nullable()->after('status');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('expenses', function (Blueprint $table) {
            $table->dropColumn('status', 1);
            $table->dropColumn('reason');
        });
    }
}

Penjelasan: Code diatas akan menambahkan dua buah field, pertama status dengan tipe char dan yang kedua adalah reason untuk menampung alasan penolakan dari permintaan user.

Kemudian jalankan command:

php artisan migrate

Masih pada command line, buat controller dengan command:

php artisan make:controller API/ExpensesController

Kemudian buat juga resources collection:

php artisan make:resource ExpenseCollection

Buka file ExpenseCollection.php yang berada di dalam folder App/Http/Resources dan modifikasi menjadi:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class ExpenseCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection
        ];
    }
}

Saatnya kita mengerjakan API untuk menampilkan data expenses, buka file ExpensesController.php dan tambahkan method:

public function index()
{
    $user = request()->user(); //AMBIL DATA USER YANG SEDANG LOGIN
    $expenses = Expense::with(['user'])->orderBy('created_at', 'DESC'); //GET DATA EXPENSES YANG DIURUTKAN BERDASARKAN DATA TERBARU BESERTA SEBUAH EAGER LOADING UNTUK MENGAMBIL SIAPA PEMILIK DATA TERSEBUT

    //APABILA ADA PENCARIAN
    if (request()->q != '') {
        //MAKA AMBIL DATA BERDASARKAN PENCARIAN YANG DILAKUKAN
        $expenses = $expenses->where('description', 'LIKE', '%' . request()->q . '%');
    }

    //JIKA ROLE USER YANG LOGIN ADALAH 1 (ADMIN) & 3 (KURIR), MAKA AMBIL DATA KHUSUS DATA MEREKA SAJA
    if (in_array($user->role, [1, 3])) {
        $expenses = $expenses->where('user_id', $user->id);
    }
    return (new ExpenseCollection($expenses->paginate(10)));
}

Masih dengan file yang sama, tambahkan use statement:

use App\Http\Resources\ExpenseCollection;
use App\Expense;

Selanjutnya adalah membuat routing untuk API-nya, buka file routes/api.php dan tambahkan route dibawah ini:

Route::resource('expenses', 'API\ExpensesController')->except(['create', 'show']);

Sebelum berpindah pada sisi client, buka file app/Expense.php dan modifikasi menjadi:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Expense extends Model
{
    protected $guarded = [];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Satu hal lagi, agar nama user dimulai dengan huruf kapital maka kita perlu membuat Accessor. Buat file app/User.php dan tambahkan method:

public function getNameAttribute($value)
{
    return ucfirst($value);
}

API sudah selesai, maka saatnya untuk membuat halaman yang akan menggunakan API tersebut, buka file resources/js/router.js dan tambahkan route berikut:

{
    path: '/expenses',
    component: IndexExpenses,
    meta: { requiresAuth: true },
    children: [
        {
            path: '',
            name: 'expenses.data',
            component: DataExpenses,
            meta: { title: 'Manage Expenses' }
        }
    ]
}

Pada file yang sama tambahkan line berikut pada bagian import statement:

import IndexExpenses from './pages/expenses/Index.vue'
import DataExpenses from './pages/expenses/Expenses.vue'

Kemudian buat file Index.vue di dalam folder /pages/expenses dan tambahkan code:

<template>
    <div class="container">
        <section class="content-header">
            <h1>
                Manage Expenses
            </h1>
            <breadcrumb></breadcrumb>
        </section>

        <section class="content">
            <div class="row">
                <router-view></router-view>
            </div>
        </section>
    </div>
</template>
<script>
    import Breadcrumb from '../../components/Breadcrumb.vue'
    export default {
        name: 'IndexExpenses',
        components: {
            'breadcrumb': Breadcrumb
        }
    }
</script>

Note: Tidak ada yang perlu dijelaskan karena sama dengna artikel sebelumnya.

Lalu file selanjutnya adalah Expenses.vue didalam folder yang sama.

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <router-link :to="{ name: 'expenses.create' }" class="btn btn-primary btn-sm btn-flat">Tambah</router-link>
                <div class="pull-right">
                    <input type="text" class="form-control" placeholder="Cari..." v-model="search">
                </div>
            </div>
            <div class="panel-body">
              
              	<!-- TABLE INI AKAN MENAMPILKAN DATA EXPENSES -->
                <b-table striped hover bordered :items="expenses.data" :fields="fields" show-empty>
                    <template slot="status" slot-scope="row">
                        <span class="label label-success" v-if="row.item.status == 1">Diterima</span>
                        <span class="label label-warning" v-else-if="row.item.status == 0">Diproses</span>
                        <span class="label label-default" v-else>Ditolak</span>
                    </template>
                    <template slot="user" slot-scope="row">
                        {{ row.item.user.name }}
                    </template>
                    <template slot="reason" slot-scope="row">
                        {{ row.item.reason == '' ? '-':row.item.reason }}
                    </template>
                    <template slot="actions" slot-scope="row">
                        <router-link :to="{ name: 'expenses.edit', params: {id: row.item.id} }" class="btn btn-warning btn-sm" v-if="row.item.status == 0"><i class="fa fa-pencil"></i></router-link>
                        <router-link :to="{ name: 'expenses.view', params: {id: row.item.id} }" class="btn btn-info btn-sm"><i class="fa fa-eye"></i></router-link>
                        <button class="btn btn-danger btn-sm" @click="deleteExpenses(row.item.id)" v-if="row.item.status == 0"><i class="fa fa-trash"></i></button>
                    </template>
                </b-table>
                <!-- TABLE INI AKAN MENAMPILKAN DATA EXPENSES -->

                <div class="row">
                  	<!-- BAGIAN INI AKAN MENAMPILKAN INFORMASI DATA DAN PAGINATION -->
                    <div class="col-md-6">
                        <p v-if="expenses.data"><i class="fa fa-bars"></i> {{ expenses.data.length }} item dari {{ expenses.meta.total }} total data</p>
                    </div>
                    <div class="col-md-6">
                        <div class="pull-right">
                            <b-pagination
                                v-model="page"
                                :total-rows="expenses.meta.total"
                                :per-page="expenses.meta.per_page"
                                aria-controls="expenses"
                                v-if="expenses.data && expenses.data.length > 0"
                                ></b-pagination>
                        </div>
                    </div>
                    <!-- BAGIAN INI AKAN MENAMPILKAN INFORMASI DATA DAN PAGINATION -->
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { mapActions, mapState } from 'vuex'

export default {
    name: 'DataExpenses',
    created() {
        this.getExpenses() //KETIKA HALAMAN DI-LOAD MAKA FUNGSI INI AKAN DIJALANKAN
    },
    data() {
        return {
            //UNTUK FIELD TABLE DIATAS YANG AKAN DITAMPILKAN
            fields: [
                { key: 'description', label: 'Permintaan' },
                { key: 'price', label: 'Biaya' },
                { key: 'note', label: 'Catatan' },
                { key: 'user', label: 'Kurir/Admin' },
                { key: 'status', label: 'Status' },
                { key: 'reason', label: 'Alasan' },
                { key: 'actions', label: 'Aksi' }
            ],
            search: '' //VARIABLE UNTUK SEARCH
        }
    },
    computed: {
        ...mapState('expenses', {
            expenses: state => state.expenses //AMBIL STATE DARI MODULE EXPENSES YANG AKAN DIBUAT KEMUDIAN
        }),
        //AMBIL DAN MODIFIKASI STATE PAGE JIKA TERJADI PERUBAHAN
        page: {
            get() {
                return this.$store.state.expenses.page
            },
            set(val) {
                this.$store.commit('expenses/SET_PAGE', val)
            }
        }
    },
    watch: {
        //KETIKA TERJADI PERUBAHAN VALUE STATE PAGE 
        page() {
            this.getExpenses() //MAKA JALANKAN FUNGSI INI
        },
        //KETIKA TERJADI PERUBAHAN VALUE VARIABLE SEARCH 
        search() {
            this.getExpenses(this.search) //MAKA JALANKAN FUNGSI INI DGN MENGIRIMKAN VALUE DARI SEARCH
        }
    },
    methods: {
        ...mapActions('expenses', ['getExpenses', 'removeExpenses']), //DEFINISIKAN ACTIONS DARI MODULE EXPENSES
        //FUNGSI INI SAMA DENGAN MODULE SEBELUMNYA UNTUK MENAMPILKAN ALERT KETIKA MENGHAPUS DATA
        deleteExpenses(id) {
            this.$swal({
                title: 'Kamu Yakin?',
                text: "Tindakan ini akan menghapus secara permanent!",
                type: 'warning',
                showCancelButton: true,
                confirmButtonColor: '#3085d6',
                cancelButtonColor: '#d33',
                confirmButtonText: 'Iya, Lanjutkan!'
            }).then((result) => {
                if (result.value) {
                    //JIKA DISETUJUI MAKA FUNGSI INI DIJALANKAN
                    this.removeExpenses(id)
                }
            })
        }
    }
}
</script>

Untuk mengelola request ke backend, maka kita memerlukan bantuan vuex, buat file expenses.jsdi dalam folder resources/js/stores dan tambahkan code:

import $axios from '../api.js'

const state = () => ({
    expenses: [], //UNTUK MENAMPUNG DATA EXPENSES
    page: 1 //STATE UNTUK HALAMAN YANG SEDANG AKTIF
})

const mutations = {
    //ASSIGN DATA EXPENSES YANG DIDAPATKAN KE DALAM STATE
    ASSIGN_DATA(state, payload) {
        state.expenses = payload
    },
    //SET PAGE YANG AKTIF KE DALAM STATE PAGE
    SET_PAGE(state, payload) {
        state.page = payload
    }
}

const actions = {
    //FUNGSI UNTUK MENG-HANDLE REQUEST KE BACKEND
    getExpenses({ commit, state }, payload) {
        let search = typeof payload != 'undefined' ? payload:''
        return new Promise((resolve, reject) => {
            //KIRIM PERMINTAAN KE BACKEND 
            $axios.get(`/expenses?page=${state.page}&q=${search}`)
            .then((response) => {
                //KETIKA RESPONSE NYA DIDAPATKAN, MAKA ASSIGN DATA TERSEBUT KE STATE
                commit('ASSIGN_DATA', response.data)
                resolve(response.data)
            })
        })
    }
}

export default {
    namespaced: true,
    state,
    actions,
    mutations
}

Agar module vuex diatas dapat digunakan, daftar terlebih dahulu dengan membuka file store.js dan tambahkan bagian berikut:

modules: {
    auth,
    outlet,
    courier,
    product,
    user,
    expenses //TAMBAHKAN BAGIAN INI
},

Jangan lupa di import pada bagian import statement:

import expenses from './stores/expenses.js'

Terakhir dari bagian ini, buka file Header.vue untuk menambahkan link pada menu navigasi agar lebih mudah untuk mengaksesnya melakukan menu navigasi. Tambahkan code pada bagian tag html menu-nya:

<li><router-link :to="{ name: 'expenses.data' }">Expenses</router-link></li>

Add New Expenses

Hal yang menarik akan kita mulai dari module ini, dimana ketika admin/kurir menambahkan data permintaan biaya operasional maka secara otomatis akan mengirimkan notifikasi ke bagian finance dan superadmin. Pertama kita buat dulu form input-nya, buat file Add.vue di dalam folder /pages/expenses dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Add New Expenses</h3>
            </div>
            <div class="panel-body">
              	<!-- TAMBAHKAN REF PADA CUSTOM TAG INI -->
                <expenses-form ref="formExpense"></expenses-form>
              
                <div class="form-group">
                    <button class="btn btn-primary btn-sm btn-flat" @click.prevent="submit">
                        <i class="fa fa-save"></i> Add New
                    </button>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    import FormExpenses from './Form.vue'
    export default {
        name: 'AddExpenses',
        methods: {
            //KETIKA TOMBOL ADD NEW DITEKAN MAKA FUNGSI INI AKAN DIJALANKAN
            submit() {
                //FUNGSI INI AKAN MEMICU METHOD YANG BERADA DIDALAM FILE FORM.VUE
                //DENGAN MEMANFAATKAN $refs MAKAN KITA DAPAT MENGAKSES PROPERTY YANG ADA DI FILE LAINNYA
                this.$refs.formExpense.submit()
            }
        },
        components: {
            'expenses-form': FormExpenses
        }
    }
</script>

Kemudian buat file Form.vue di dalam folder yang sama dan tambahkan code:

<template>
    <div>
        <div class="form-group" :class="{ 'has-error': errors.description }">
            <label for="">Permintaan</label>
            <input type="text" class="form-control" v-model="expenses.description">
            <p class="text-danger" v-if="errors.description">{{ errors.description[0] }}</p>
        </div>
        <div class="form-group" :class="{ 'has-error': errors.price }">
            <label for="">Biaya</label>
            <input type="number" class="form-control" v-model="expenses.price">
            <p class="text-danger" v-if="errors.price">{{ errors.price[0] }}</p>
        </div>
        <div class="form-group" :class="{ 'has-error': errors.note }">
            <label for="">Catatan</label>
            <textarea cols="5" rows="5" class="form-control" v-model="expenses.note"></textarea>
            <p class="text-danger" v-if="errors.note">{{ errors.note[0] }}</p>
        </div>
    </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'
export default {
    name: 'FormExpenses',
    created() {
        //KARENA FILE INI KITA BUAT REUSABLE, BISA DIGUNAKAN UNTUK ADD DAN EDIT
        //MAKA KETIKA COMPONENT INI DI-LOAD DARI HALAMAN EDIT EXPANSES
        if (this.$route.name == 'expenses.edit') {
            //FUNGSI UNTUK MENGAMBIL SINGLE DATA YANG AKAN DIEDIT DIJALANKAN
            //ADAPUN FUNGSINYA AKAN KITA BUAT SAAT MEMBAHAS FITUR EDIT
            this.editExpenses(this.$route.params.id).then((res) => {
                //RESPONSENYA KITA MASUKKAN KE DALAM VARIABLE YANG TELAH DIDEFINISIKAN
                this.expenses = {
                    description: res.data.description,
                    price: res.data.price,
                    note: res.data.note
                }
            })
        }
    },
    data() {
        return {
            expenses: {
                description: '',
                price: '',
                note: ''
            }
        }
    },
    computed: {
        ...mapState(['errors']) //MENGAMBIL STATE ERROR
    },
    methods: {
        //MENDEFINISIKAN ACTION DARI MODULE EXPENSES VUEX
        ...mapActions('expenses', ['submitExpense', 'editExpenses', 'updateExpenses']),
        submit() {
            //KETIKA FUNGSI INI BERJALAN (NOTE: DI PICU DARI FILE ADD TADI)
            //DI CEK DARI HALAMAN MANA
            if (this.$route.name == 'expenses.edit') {
                //JIKA DARI EDIT, MAKA KITA ASSIGN ID YANG AKAN DI EDIT KE DALAM 
                //OBJECT VARIABLE EXPENSES
                let data = Object.assign({id: this.$route.params.id}, this.expenses)
                //KEMUDIAN KIRIM PERMINTAAN UPDATE DATA KE BACKEND
                this.updateExpenses(data).then(() => this.$router.push({name: 'expenses.data'}))
            } else {
                //JIKA DARI HALAMAN ADD NEW EXPENSES
                //MAKA LANGSUNG MENGIRIMKAN PERMINTAAN UNTUK MENAMBAHKAN DATA
                this.submitExpense(this.expenses).then(() => this.$router.push({ name: 'expenses.data' }))
            }
        }
    },
    destroyed() {
        //KETIKA COMPONENT DITINGGALKAN, MAKA VARIABLENYA DI KOSONGKAN
        this.expenses = {
            description: '',
            price: '',
            note: ''
        }
    }
}
</script>

Karena fungsi untuk mengambil single data yang akan di-edit dan di-update telah disinggung diatas, maka kita definisikan juga fungsinya sekalian agar tidak terjadi error ketika halaman add new dibuka. Buka file stores/expenses.js dan tambahkan code berikut pada bagian actions:

submitExpense({ dispatch, commit }, payload) {
    return new Promise((resolve, reject) => {
        //KIRIM PERMINTAAN UNTUK MENAMBAHKAN DATA DENGAN METHOD POST
        $axios.post(`/expenses`, payload)
        .then((response) => {
            //AMBIL DATA YANG BARU
            dispatch('getExpenses').then(() => {
                resolve(response.data)
            })
        })
        .catch((error) => {
            //JIKA VALIDASI ERROR
            if (error.response.status == 422) {
                //MAKA ERRORNYA DI ASSIGN KE STATE ERRORS
                commit('SET_ERRORS', error.response.data.errors, { root: true })
            }
        })
    })
},
//FUNGSI INI UNTUK MENGAMBIL SINGLE DATA
editExpenses({ commit }, payload) {
    return new Promise((resolve, reject) => {
        //MENGIRIMKAN PERMINTAAN KE BACKEND UNTUK MENGAMBIL DATA BERDASARKAN ID
        $axios.get(`/expenses/${payload}/edit`)
        .then((response) => {
            resolve(response.data)
        })
    })
},
//FUNGSI INI UNTUK MENGUPDATE DATA
updateExpenses({ commit }, payload) {
    return new Promise((resolve, reject) => {
        //MENGIRIMKAN PERMINTAAN KE SERVER UNTUK MENGUBAH DATA BERDASARKAN ID
        $axios.put(`/expenses/${payload.id}`, payload)
        .then((response) => {
            resolve(response.data)
        })
    })
},

Ets jangan buru-buru, kita belum mendefinisikan route untuk halaman add data. Buka file router.js dan tambahkan code ini di dalam children dari path /expenses

{
    path: 'add',
    name: 'expenses.create',
    component: CreateExpenses,
    meta: { title: 'Add New Expenses' }
},

Pastikan kita juga menambahkan bagian ini pada import statement di dalam file yang sama:

import CreateExpenses from './pages/expenses/Add.vue'

Dari sisi client sudah selesai, saatnya untuk berpindah pada sisi backend. Buka file ExpensesController.php dan tambahkan method:

public function store(Request $request)
{
    //VALIDASI DATA YANG DIKIRIM
    $this->validate($request, [
        'description' => 'required|string|max:150',
        'price' => 'required|integer',
        'note' => 'nullable|string'
    ]);

    $user = $request->user(); //GET USER YANG SEDANG LOGIN
    //JIKA USER YANG SEDANG LOGIN ROLENYA SUPERADMIN/FINANCE
    //MAKA SECARA OTOMATIS DI APPROVE, SELAIN ITU STATUSNYA 0 BERARTI HARUS
    //MENUNGGU PERSETUJUAN
    $status = $user->role == 0 || $user->role == 2 ? 1:0; 
    //TAMBAHKAN USER_ID DAN STATUS KE DALAM REQUEST
    $request->request->add([
        'user_id' => $user->id,
        'status' => $status
    ]);

    //KEMUDIAN BUAT RECORD BARU KE DALAM DATABASE
    $expenses = Expense::create($request->all());

    //GET USER YANG ROLE-NYA SUPERADMIN DAN FINANCE
    //KENAPA? KARENA HANYA ROLE ITULAH YANG AKAN MENDAPATKAN NOTIFIKASI
    $users = User::whereIn('role', [0, 2])->get();
    //KIRIM NOTIFIKASINYA MENGGUNAKAN FACADE NOTIFICATION
    Notification::send($users, new ExpensesNotification($expenses, $user));

    return response()->json(['status' => 'success']);
}

Masih dengan file yang sama, tambahkan use statement:

use Illuminate\Support\Facades\Notification;
use App\Notifications\ExpensesNotification;
use App\User;

Tugas kita selanjutnya adalah men-generate file ExpensesNotification.php, pada command line jalankan command:

php artisan make:notification ExpensesNotification

Buka file tersebut yang berada di dalam folder app/Notifications dan modifikasi menjadi:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\BroadcastMessage;


class ExpensesNotification extends Notification implements ShouldQueue
{
    use Queueable;

		//DEFINSIKAN GLOBAL VARIABLE
    protected $expenses;
    protected $user;
    public function __construct($expenses, $user)
    {
        //ASSIGN DATA YANG DITERIMA KE DALAM GLOBAL VARIABLE
        $this->expenses = $expenses;
        $this->user = $user;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        //JADI KITA MENGGUNAKAN DUA METHOD, SIMPAN KE DATABASE DAN BROADCAST KE PUSHER
        //PUSHER ADALAH PIHAK KETIGA YANG AKAN DIGUNAKAN UNTUK REALTIME NOTIFICATION
        return ['database', 'broadcast'];
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
  
    //FORM DATA YANG DISIMPAN KE DALAM DATABASE
    public function toDatabase($notifiable)
    {
        return [
            'sender_id' => $this->user->id,
            'sender_name' => $this->user->name,
            'expenses' => $this->expenses
        ];
    }

    //FORM DATA YANG AKAN DI BROADCAST
    public function toBroadcast($notifiable)
    {
        return new BroadcastMessage([
            'sender_id' => $this->user->id,
            'sender_name' => $this->user->name,
            'expenses' => $this->expenses
        ]);
    }
  
    //SEBENARNYA JIKA TIDAK ADA PERBEDAAN FORM DATA, KITA BISA LANGSUNG MENGGUNAKAN SATU METHOD SAJA YAKNI toArray()
}

Agar data broadcast hanya dapat diakses oleh user yang sudah login saja, maka buka file app/Providers/BroadcastServiceProvider dan modifikasi code ini:

Broadcast::routes();

//MEJADI

Broadcast::routes(['middleware' => 'auth:api']);

Kemudian buka file config/app.php dan uncomment code ini:

App\Providers\BroadcastServiceProvider::class,

Adapun konfigurasi sebelumnya bahwa kita juga akan menyimpan notifikasi yang di-broadcast ke dalam database, maka generate migration-nya terlebih dahulu dengan command:

php artisan notifications:table
php artisan migrate

Register an Account In Pusher

Sebelum melanjutkan codingan kita, hal yang harus dilakukan adalah membuat akun di Pusher karena kita membutuhkan pihak ketiga ini untuk membuat realtime notifications. Ada beberapa provider selain Pusher, tapi pada seri ini kita akan menggunakan provider tersebut.

  1. Buka halaman Registrasi.
  2. Kemudian signup akun baru / bisa menggunakan signup via sosmed/github.
  3. Kemudian login ke halaman dashboardnya.

Biasanya secara otomatis akan ada popup untuk membuat app, tapi jika popup-nya tidak auto, klik menu create new app pada sidebar menu. Kemudian masukkan nama app yang kamu inginkan, disini saya menggunakan name "Daengweb" dan pilih cluster-nya dimana saya memilih ap1 (Singpore) karena paling dekat. Terakhir klik tombol Create my app.

Secara otomatis kamu akan diarahkan ke halaman channel yang telah kamu buat dan jika tidak bisa dengan mengunjungi menu Channels apps pada sidebar menu. Setelah itu, kunjungi tab Getting started dan pada sisi kanan kita akan melihat channel credentials seperti berikut:

PUSHER_APP_ID=831xxx
PUSHER_APP_KEY=f6526f8fxxxxxxx
PUSHER_APP_SECRET=477d72e8exxxxxxx

Buka file .env dan masukkan key tersebut pada code dibawah:

BROADCAST_DRIVER=pusher

PUSHER_APP_ID=masukkan id kamu disini
PUSHER_APP_KEY=masukkan key kamu disini
PUSHER_APP_SECRET=masukkan secret kamu disini
PUSHER_APP_CLUSTER=ap1

Note: Semua variable diatas sudah tersedia, kita hanya perlu mengubah value-nya saja.

Create Listners Broadcasting

Backend telah menyelesaikan tugasnya untuk mengirimkan broadcast, maka selanjutnya kita akan membuat fungsi dari sisi client untuk meng-handle setiap ada broadcast yang diterima dari Pusher. Buka file app.js dan tambahkan methods:

computed: {
    ...mapGetters(['isAuth']),
    ...mapState(['token']), //GET TOKEN
    ...mapState('user', {
        user_authenticated: state => state.authenticated //MENGAMBIL STATE USER YANG SEDANG LOGIN
    })
},
methods: {
    ...mapActions('user', ['getUserLogin']),
    ...mapActions('notification', ['getNotifications']), //DEFINISIKAN FUNGSI UNTUK MENGAMBIL NOTIFIKASI DARI TABLE NOTIFICATIONS
    ...mapActions('expenses', ['getExpenses']), //FUNGSI UNTUK MENGAMBIL EXPENSES YANG BARU
      //METHOD INI UNTUK MENGISIASI LISTER MENGGUNAKAN LARAVEL ECHO
      initialLister() {
        //JIKA USER SUDAH LOGIN
        if (this.isAuth) {
            //MAKA INISIASI FUNGSI BROADCASTER DENGAN KONFIGURASI BERIKUT
            window.Echo = new Echo({
                broadcaster: 'pusher',
                key: process.env.MIX_PUSHER_APP_KEY, //VALUENYA DI AMBIL DARI FILE .ENV
                cluster: process.env.MIX_PUSHER_APP_CLUSTER,
                encrypted: false,
                auth: {
                    headers: {
                        Authorization: 'Bearer ' + this.token
                    },
                },
            });

            if (typeof this.user_authenticated.id != 'undefined') {
                //KEMUDIAN KITA MENGAKSES CHANNEL BROADCAST SECARA PRIVATE
                window.Echo.private(`App.User.${this.user_authenticated.id}`)
                .notification(() => {
                    //APABILA DITEMUKAN, MAKA KITA MENJALANKAN KEDUA FUNGSI INI
                    //UNTUK MENGAMBIL DATA TERBARU
                    this.getNotifications()
                    this.getExpenses()
                })
            }
        }
    }
},
watch: {
    //KITA WATCH KETIKA VALUE TOKEN BERUBAH, MAKA 
    token() {
        this.initialLister() //KITA JALANKAN FUNGSI UNTUK MENGINISIASI LAGI
    },
    //KETIKA VALUE USER YANG SEDANG LOGIN BERUBAH
    user_authenticated() {
        this.initialLister() //KITA JALANKAN LAGI
    }
},
created() {
    if (this.isAuth) {
        this.getUserLogin()
        //TAMBAHKAN BAGIAN INI KETIKA COMPONENT DILOAD
        this.initialLister()
        this.getNotifications()
    }
}

Note: App.User.${this.user_authenticated.id} adalah channel yang dapat ditemukan pada file routes/channel.php, fungsinya untuk memastikan bahwa user yang login benar karena kita menggunakan private channel dimana hanya user yang memiliki akses yang dapat mengambil data broadcast.

Masih dengan file yang sama, pada bagian import statement tambahkan code berikut:

import { mapActions, mapGetters, mapState } from 'vuex'
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'

Pada command line, install package ini:

composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js

Sebenarnya sampai pada tahap ini kita telah berhasil menyelesaikan persiapan untuk mengawasi broadcast yang dikirimkan oleh Laravel ke Pusher dan client sudah dapat mendeteksi ketika terdapat data yang baru pada channel Pusher. Akan tetapi terdapat beberapa bagian yang error karena module Vuex-nya belum kita buat, maka tugas selanjutnya akan dibahas pada sub-chapter berikutnya.

Show Notifications

Metode yang digunakan adalah via broadcast dan database, maka listening channel hanya kita gunakan untuk mengetahui apakah ada data yang diterima dari channel yang sedang di awasi atau tidak, dan dari informasi tersebut digunakan untuk melakukan request ke server guna mengambil data notifikasi yang telah disimpan ke database. Jika kita lihat codingan sebelumnya, kita melakukan dua buah request yakni menjalankan method getNotifications() dan getExpenses(), namun method kedua sudah dibuat sebelumnya dan diletakkan ke dalam module expenses dari Vuex.

Tugas kita kali ini adalah membuat module notifications guna menampung actions getNotifications(). Buat file notification.js dan tempatkan di dalam folder resources/js/stores.

import $axios from '../api.js'

const state = () => ({
    notifications: [] //MENAMPUNG DATA NOTIFIKASI
})

const mutations = {
    //ASSIGN DATA NOTIFIKASI KE DALAM STATE NOTIFICATIONS
    ASSIGN_DATA(state, payload) {
        state.notifications = payload
    }
}

const actions = {
    getNotifications({ commit }) {
        return new Promise((resolve, reject) => {
            //REQUEST KE SERVER UNTUK MENGAMBIL NOTIFIKASI
            $axios.get(`/notification`)
            .then((response) => {
                //DATA YANG DITERIMA DI COMMIT KE MUTATIONS ASSING_DATA
                commit('ASSIGN_DATA', response.data.data)
                resolve(response.data)
            })
        })
    },
    readNotification({ dispatch }, payload) {
        return new Promise((resolve, reject) => {
            //UNTUK MENGUPDATE DATA NOTIFIKASI BAHWA NOTIF TERSEBUT SUDAH DIBACA
            $axios.post(`/notification`, payload)
            .then((response) => {
                //AMBIL DATA NOTIFIKASI TERBARU
                dispatch('getNotifications').then(() => resolve(response.data))
            })
        })   
    }
}

export default {
    namespaced: true,
    state,
    actions,
    mutations
}

Agar file diatas dapat digunakan, daftarkan ke dalam file store.js:

modules: {
    auth,
    outlet,
    courier,
    product,
    user,
    expenses,
    notification //TAMBAHKAN BAGIAN INI
},

Jangan lupa import pada bagian import statement:

import notification from './stores/notification.js'

Pindah ke sisi Backend, buat sebuah controller baru dengan command:

php artisan make:controller API/NotificationController

Kemudian buka file NotificationController.php dan modifikasi menjadi:

<?php

namespace App\Http\Controllers\API;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class NotificationController extends Controller
{
    public function index()
    {
        $user = request()->user(); //AMBIL USER YANG SEDANG LOGIN
        //KEMUDIAN YANG DI READ ADALAH HANYA NOTIFIKASI YANG STATUSNYA BELUM DIREAD
        //SECARA LANGSUNG KITA DAPAT MENGAMBIL DATA NYA MELALUI USER DENGAN MENGAKSES PROPERTY unreadNotifications.
        return response()->json(['status' => 'success', 'data' => $user->unreadNotifications]);
    }

    public function store(Request $request)
    {
        $user = $request->user(); //AMBIL USER YANG SEDANG LOGIN
        //EDIT RECORD NOTIFIKASI BERDASARKAN ID YANG DITERIMA
        //NOTIFIKASI YANG SUDAH/BELUM DIREAD DITANDAI DENGAN read_at YANG MASIH NULL
        $user->unreadNotifications()->where('id', $request->id)->update(['read_at' => now()]);
        return response()->json(['status' => 'success']);
    }
}

Jangan lupa definisikan routing-nya, buka file routes/api.php dan tambahkan:

Route::resource('notification', 'API\NotificationController')->except(['create', 'destroy']);

Kembali lagi ke sisi Client, kita akan meng-handle data notifikasi tersebut dengan menampilkannya pada Header navbar. Buka file components/Header.vue dan modifikasi beberapa bagian berikut:

<li class="dropdown messages-menu">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        <i class="fa fa-bell-o"></i>
        
        <!-- FUNGSI INI UNTUK MENGHITUNG JUMLAH DATA NOTIFIKASI YANG ADA -->
        <span class="label label-success">{{ notifications.length }}</span>
    </a>
    <ul class="dropdown-menu">
        <li class="header">You have {{ notifications.length }} messages</li>
        <li>
            <ul class="menu" v-if="notifications.length > 0">
                
                <!-- KITA MELAKUKAN LOOPING TERHADAP DATA NOTIFIKASI YANG DISIMPAN KE DALAM STATE NOTIFICATIONS -->
                <li v-for="(row, index) in notifications" :key="index">
                    <a href="javascript:void(0)" @click="readNotif(row)">
                        <div class="pull-left">
                            <img src="https://via.placeholder.com/160" class="img-circle" alt="User Image">
                        </div>
                        <h4>
                            <!-- TAMPILKAN NAMA PENGIRIM NOTIFIKASI -->
                            {{ row.data.sender_name }}
                            <!-- TAMPILKAN WAKTU NOTIFIKASI -->
                            <small><i class="fa fa-clock-o"></i> {{ row.created_at | formatDate }}</small>
                        </h4>
                        <!-- TAMPILKAN JENIS PERMINTAAN NOTIFIKASI -->
                        <p>{{ row.data.expenses.description.substr(0, 30) }}</p>
                    </a>
                </li>
            </ul>
        </li>
        <!-- <li class="footer"><a href="#">See All Messages</a></li> -->
    </ul>
</li>

Note: Adapun icon informasi lainnya kita disable saja, boleh dihapus / di-comment.

Masih dengan file yang sama, bergeser pada bagian javascript-nya, tambahkan code berikut:

computed: {
    ...mapState('user', {
        authenticated: state => state.authenticated
    }),
    
    //CUKUP TAMBAHKAN BAGIAN INI
    ...mapState('notification', {
        notifications: state => state.notifications //MENGAMBIL STATE NOTIFICATIONS
    })
},
//TAMBAHKAN JUGA BAGIAN INI
filters: {
    //UNTUK MENGUBAH FORMAT TANGGAL MENJADI TIME AGO
    formatDate(val) {
        return moment(new Date(val)).fromNow()
    }
},
methods: {
    ...mapActions('notification', ['readNotification']), //DEFINISIKAN FUNGSI UNTUK READ NOTIF
    
    //KETIKA NOTIFIKASI DI KLIK MAKA AKAN MENJALANKAN FUNGSI INI
    readNotif(row) {
        //MENGIRIMKAN REQUEST KE SERVER UNTUK MENANDAI BAHWA NOTIFIKASI TELAH DI BACA
        //KEMUDIAN SELANJUTNYA KITA REDIRECT KE HALAMAN VIEW EXPENSES
        this.readNotification({ id: row.id}).then(() => this.$router.push({ name: 'expenses.view', params: {id: row.data.expenses.id} }))
    },
    
    //[.. CODE SETELAHNYA ..]
}

Lagi lagi dengan file yang sama, tambahkan bagi ini pada import statement:

import { mapState, mapActions } from 'vuex'
import moment from 'moment'

Jangan lupa untuk meng-install momentJs

npm install moment --save

View Detail Expenses

Menampilkan detail expenses dimana terdapat fitur untuk menyetujui atau menolak permintaan tersebut akan menjadi tugas kita pada bagian ini. Buat file View.vue di dalam folder /pages/expenses dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Detail Expenses</h3>
            </div>
            <div class="panel-body">
                <template>
                  	<!-- INFORMASI DETAIL EXPENSES -->
                    <dt>Permintaan Karyawan</dt>
                    <dd>- {{ description }}</dd>

                    <hr>
                    <dt>Biaya Yang Diperlukan</dt>
                    <dd>- Rp {{ price }}</dd>
                    <hr>

                    <dt>Catatan</dt>
                    <dd>- {{ note }}</dd>
                    <hr>

                    <dt>User/Kurir</dt>
                    <dd>- {{ user.name }}</dd>
                    <hr>

                    <dt>Status</dt>
                    <dd>
                        <span class="label label-success" v-if="status == 1">Diterima</span>
                        <span class="label label-warning" v-else-if="status == 0">Diproses</span>
                        <span class="label label-default" v-else>Ditolak</span>
                    </dd>
                    <hr>
                    <!-- INFORMASI DETAIL EXPENSES -->

                    <!-- JIKA STATUSNYA 2 = CANCEL, MAKA ALASANNYA DITAMPILKAN -->
                    <div v-if="status == 2">
                        <dt>Alasan Penolakan</dt>
                        <dd>- {{ reason }}</dd>
                        <hr>
                    </div>
                  
                    <!-- JIKA STATUS 0 = BARU ATAU BARU DAN formReason = false MAKA TOMBOL TOLAK DAN TERIMA DITAMPILKAN -->
                    <div class="pull-right" v-if="status == 0 || (status == 0 && !formReason)">
                        <!-- KETIKA TOMBOL INI DITEKAN MAKA AKAN MENGUBAH VALUE formReason JADI TRUE -->
                        <button class="btn btn-danger btn-sm" @click="formReason = true">Tolak</button>
                        <button class="btn btn-primary btn-sm" @click="accept">Terima</button>
                    </div>
                </template>

                <!-- JIKA formReason NILAINYA TRUE, MAKA FORM INI DITAMPILAKN UNTUK MENGISI ALASAN PENOLAKAN -->
                <div v-if="formReason">
                    <div class="form-group">
                        <label for="">Alasan Penolakan</label>
                        <input type="text" v-model="inputReason" class="form-control">
                    </div>
                    <div class="form-group">
                        <button class="btn btn-primary btn-sm pull-right" @click="cancelRequest">Respon Penolakan</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { mapActions } from 'vuex';
    export default {
        name: 'ViewEpenses',
        created() {
            //KETIKA COMPONENT INI DI-LOAD MAKA KITA MENGAMBIL SINGLE DATA YANG AKAN DI TAMPILKAN BERDASARKAN ID PADA URL
            this.editExpenses(this.$route.params.id).then((res) => {
                let row = res.data
                //ASSIGN SEMUA DATANYA KE DALAM VARIABLE YANG TELAH DIDEFINISIKAN
                this.description =  row.description
                this.price =  row.price
                this.note =  row.note
                this.status =  row.status
                this.reason =  row.reason
                this.user = row.user
            })
        },
        data() {
            return {
                description: '',
                price: '',
                note: '',
                status: '',
                reason: '',
                user: '',
                formReason: false,
                inputReason: ''
            }
        },
        methods: {
            ...mapActions('expenses', ['editExpenses', 'acceptExpenses', 'cancelExpenses']),
            //KETIKA TOMBOL TERIMA DITEKAN, MAKA AKAN MENJALANKAN FUNGSI accpet
            accept() {
                this.$swal({
                    title: 'Kamu Yakin?',
                    text: "Permintaan yang disetujui tidak dapat dikembalikan!",
                    type: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: 'Iya, Lanjutkan!'
                }).then((result) => {
                    if (result.value) {
                        //JIKA YES, MAKA KITA AKAN MENGIRIMKAN PERMINTAAN KE SERVER UNTUK MENYETUJUI PERMINTAAN TERSEBUT DAN REDIRECT KE HALAMAN LIST EXPENSES
                        this.acceptExpenses(this.$route.params.id).then(() => this.$router.push({ name: 'expenses.data' }))
                    }
                })
            },
          
            //KETIKA TOMBOL RESPON PENOLAK DITEKAN, MAKA FUNGSI INI AKAN DIJALANKAN
            cancelRequest() {
                this.$swal({
                    title: 'Kamu Yakin?',
                    text: "Permintaan yang ditolak tidak dapat dikembalikan!",
                    type: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: 'Iya, Lanjutkan!'
                }).then((result) => {
                    if (result.value) {
                        //JIKA IYA, MAKA KITA AKAN MENGIRIMKAN PERMINTAAN KE BACKEND UNTUK MENGUBAH STATUS EXPENSES MENJADI DITOLAK
                        this.cancelExpenses({id: this.$route.params.id, reason: this.inputReason}).then(() => {
                            this.formReason = false //FORMREASON DI SET KEMBALI JADI FALSE
                            this.$router.push({ name: 'expenses.data' }) //DAN REDIRECT KEMBALI KE HALAMAN LIST EXPENSES
                        })
                    }
                })
            }
        }
    }
</script>

Tugas selanjutnya adalah membuat actions pada module expenses.js, buka file tersebut dan tambahkan kedua method berikut pada bagian actions.

acceptExpenses({ commit }, payload) {
    return new Promise((resolve, reject) => {
        //KIRIM PERMINTAAN KE SERVER UNTUK MENGUBAH VALUE JADI ACCEPT
        $axios.post(`/expenses/accept`, { id: payload })
        .then((response) => {
            resolve(response.data)
        })
    })
},
cancelExpenses({ commit }, payload) {
    return new Promise((resolve, reject) => {
        //KIRIM PERMINTAAN KE SERVER UNTUK MENGUBAH VALUE JADI CANCEL
        $axios.post(`/expenses/cancel`, payload)
        .then((response) => {
            resolve(response.data)
        })
    })
}

Kemudian kita buat routing-nya dari sisi client, buka file router.js dan tambahkan code dibawah ini pada children dari expenses.

{
    path: 'view/:id',
    name: 'expenses.view',
    component: ViewExpenses,
    meta: { title: 'View Expenses' }
},

Jangan lupa tambahkan fungí untuk meng-import pada bagian import statement di dalam file router.js:

import ViewExpenses from './pages/expenses/View.vue'

Saatnya mengolah kembali dari sisi Backend, buka file ExpensesController.php dan tambahkan kedua method berikut:

public function accept(Request $request)
{
    $this->validate($request, ['id' => 'required|exists:expenses,id']); //VALIDASI DATA
    $expenses = Expense::with(['user'])->find($request->id); //AMBIL SINGLE DATA EXPENSES
    $expenses->update(['status' => 1]); //UBAH STATUSNYA JADI APPROVE
    Notification::send($expenses->user, new ExpensesNotification($expenses, $expenses->user)); //KIRIM NOTIFIKASI KE PEMBUAT REQUEST EXPENSES
    return response()->json(['status' => 'success']);
}

public function cancelRequest(Request $request)
{
    $this->validate($request, ['id' => 'required|exists:expenses,id', 'reason' => 'required|string']); //VALIDASI
    $expenses = Expense::with(['user'])->find($request->id); //AMBIL SINGLE DATA EXPENSES
    $expenses->update(['status' => 2, 'reason' => $request->reason]); //UBAH STATUS DAN TAMBAHKAN REASON
    Notification::send($expenses->user, new ExpensesNotification($expenses, $expenses->user)); //KIRIM NOTIFIKASI
    return response()->json(['status' => 'success']);
}

Terakhir buka file routes/api.php dan tambahkan kedua routing berikut:

Route::post('expenses/accept', 'API\ExpensesController@accept')->name('expenses.accept');
Route::post('expenses/cancel', 'API\ExpensesController@cancelRequest')->name('expenses.cancel');

Edit Data Expenses

Data Expenses hanya dapat di-edit apabila statusnya masih 0 (open) dan bagian ini sudah kita kerjakan pada sub-chapter Manage Request Lists (scroll ke atas), dimana kita menambahkan v-if="row.item.status == 0". Tugas kita disini adalah membuat halaman untuk menampilkan form edit data, buat file Edit.vue di dalam folder /pages/expenses dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Edit Expenses</h3>
            </div>
            <div class="panel-body">
                <expenses-form ref="formExpense"></expenses-form>
                <div class="form-group">
                    <button class="btn btn-primary btn-sm btn-flat" @click.prevent="submit">
                        <i class="fa fa-save"></i> Update
                    </button>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    import FormExpenses from './Form.vue'
    export default {
        name: 'EditExpenses',
        methods: {
            submit() {
                this.$refs.formExpense.submit()
            }
        },
        components: {
            'expenses-form': FormExpenses
        }
    }
</script>

Note: Disini kita tidak lagi membuat fungsi, hanya sebuah parent yang me-load file Form.vue karena fungsi-nya sudah dibuat di dalam file tersebut.

Kemudian definisikan routing-nya, buka file router.js dan tambahkan code berikut pada children expenses:

{
    path: 'edit/:id',
    name: 'expenses.edit',
    component: EditExpenses,
    meta: { title: 'Edit Expenses' }
},

Jangan lupa pada bagian import statement tambahkan code:

import EditExpenses from './pages/expenses/Edit.vue'

Adapun actions-nya juga sudah kita buat pada sub-chapter Add New Expenses (scroll ke atas), sehingga yang perlu kita lakukan hanyalah membuat API-nya saja. Buka file ExpensesController dan tambahkan method:

public function edit($id)
{
    $expenses = Expense::with(['user'])->find($id); //AMBIL DATA BERDASARKAN ID
    return response()->json(['status' => 'success', 'data' => $expenses]);
}

public function update(Request $request, $id)
{
    //VALIDASI
    $this->validate($request, [
        'description' => 'required|string|max:150',
        'price' => 'required|integer',
        'note' => 'nullable|string'
    ]);
    $expenses = Expense::find($id); //AMBIL DATA BERDASARKAN ID
    $expenses->update($request->except('id')); //UPDATE DATA TERSEBUT
    return response()->json(['status' => 'success']);
}

Delete Data Expenses

Pada sub-chapter ini menjadi lebih ringan lagi karena fungsi dari sisi client-nya sudah dikerjakan pada bagian Manage Request Lists, sehingga tugas kita hanya melengkapi actions pada module expenses untuk melakukan request ke backend. Buka file expenses.js dan tambahkan code berikut pada bagian actions.

removeExpenses({ dispatch }, payload) {
    return new Promise((resolve, reject) => {
        //KIRIM PERMINTAAN UNTUK MENGHAPUS BERDASARKAN ID
        $axios.delete(`/expenses/${payload}`)
        .then((response) => {
            //AMBIL DATA TERBARU
            dispatch('getExpenses').then(() => resolve())
        })
    })
},

Berpindah pada sisi Backend, buka ExpensesController.php dan tambahkan method:

public function destroy($id)
{
    $expenses = Expense::find($id);
    $expenses->delete();
    return response()->json(['status' => 'success']);
}

Sampai disini semua bagian telah selesai kita kerjakan, tugas terakhir adalah menjalankan command:

npm run dev

atau command

npm run watch

Kemudian testing dengan menggunakan dua buah browser yang berbeda dan salah satu browser login sebagai kurir/admin dan browser lainnya login sebagai superadmin/finance (red: fitur add admin dan finance belum ada, sehingga kamu bisa menambahkannya secara langsung ke table users terlebih dahulu atau jika tidak gunakan kurir dan superadmin saja untuk proses testing).

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #5: Manage Laundry Products

Kesimpulan

Artikel ini cukup panjang tapi juga sebanding dengan hal hal baru yang kita pelajari. Tidak hanya berinteraksi dengan proses manipulasi database tapi juga kita belajar bagaimana mengirimkan dan menerima data secara realtime tanpa harus re-load browser.

Adapun documentasi codenya dapat kamu lihat di Github.

Category:
Share:

Comments