Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #3: Management Outlets

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #3: Management Outlets

Pendahuluan

Salah satu module yang pertama kali akan dikerjakan adalah module dengan table yang tidak bergantung dengan table lainnya, maka dalam serial ke-3 ini kita akan menyelesaikan module management outlets, dimana pada module ini berfungsi untuk melakukan pendataan pada seluruh data outlet yang dimiliki oleh perusahaan laundry tersebut.

Dalam serial kali ini kita akan bermain dengan seputaran konsep CRUD menggunakan Laravel sebagai backend dan Vue.js untuk meng-handle dari sisi client dengan bantuan axios untuk berkomunikasi antar keduanya.

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #2: Templating & Authentication

Manage Outlets

Mengelola data outlets dalam sebuah halaman, berarti menampilkan seluruh data outlet yang ada agar dapat dikelola lebih lanjut, maka hal pertama yang akan kita lakukan adalah membuat sebuah halaman yang berfungsi untuk menampilkan seluruh list outlets tersebut.

Setiap terjadi proses perpindahan halaman, maka akan di-handle oleh Vue Router, maka definisikan terlebih dahulu route yang ada pada module management outlets. Buka file router.js dan tambahkan code berikut:

//[.. CODE SEBELUMNYA ..]
{
    path: '/login',
    name: 'login',
    component: Login
},

//TAMBAHKAN CODE BARU INI
{
    path: '/outlets',
    component: IndexOutlet,
    children: [
        {
            path: '',
            name: 'outlets.data',
            component: DataOutlet,
            meta: { title: 'Manage Outlets' }
        },
        {
            path: 'add',
            name: 'outlets.add',
            component: AddOutlet,
            meta: { title: 'Add New Outlet' }
        },
        {
            path: 'edit/:id',
            name: 'outlets.edit',
            component: EditOutlet,
            meta: { title: 'Edit Outlet' }
        }
    ]
}
//TAMBAHKAN CODE INI

//[.. CODE SETELAHNYA ..]

Penejelasan: Kita memiliki route /outlets dimana route ini memiliki 3 children sehingga route dari children ini akan memiliki prefix /outlets dan masing-masing route ini menggunakan component-nya sendiri. Sebagai contoh, route add path-nya akan menjadi /outlets/add.

Masih dengan file yang sama, import component yang dibutuhkan:

import IndexOutlet from './pages/outlets/Index.vue'
import DataOutlet from './pages/outlets/Outlet.vue'
import AddOutlet from './pages/outlets/Add.vue'
import EditOutlet from './pages/outlets/Edit.vue'

Terakhir dari file router.js adalah dengan menambahkan sebaris code berikut:

//[.. CODE SEBELUMNYA ..]
router.beforeEach((to, from, next) => {
    store.commit('CLEAR_ERRORS') //TAMBAHKAN BARIS INI
    if (to.matched.some(record => record.meta.requiresAuth)) {
        let auth = store.getters.isAuth
        if (!auth) {
            next({ name: 'login' })
        } else {
            next()
        }
    } else {
        next()
    }
})
//[.. CODE SETELAHNYA ..]

Penjelasan: Baris code diatas berfungsi untuk membersihkan state errors setiap kali halaman di-load.

Tinggalkan file routers.j, saatnya berpindah ke tugas selanjutnya yakni membuat 4 buah file yakni: Index.vue, Outlet.vue, Add.vue dan Edit.vue. Buat ke 4 file tersebut di dalam folder pages/outlets.

aplikasi laundry struktur direktori

Tujuan awal kita adalah menampilkan list data outlets, sehingga kita hanya akan berfokus pada file Index.vue, buka file tersebut dan tambahkan code berikut:

<template>
    <div class="container">
        <section class="content-header">
            <h1>
                Manage Outlets
            </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: 'IndexOutlet',
        components: {
            'breadcrumb': Breadcrumb
        }
    }
</script>

Penjelasan: Sebuah custom tag yang merujuk ke components Breadcrumb.vue berfungsi sebagai dynamic breadcrumb tergantung dari halaman yang sedan diakses. Ternyata file Index.vue ini hanya berfungsi sebagai parent view yang nantinya akan me-render children yang dimilikinya pada tag <router-view></router-view>.

Lalu file apa yang meng-handle list data outlets? Benar sekali, file Outlet.vue. Akan tetapi sebelum berpindahkan ke file ini, kita buat terlebih dahulu component Breadcrumb.vue di dalam folder components dan tambahkan code berikut:

<template>
    <ol class="breadcrumb">
        <li><router-link :to="{ name: 'home' }"><i class="fa fa-dashboard"></i> Home</router-link>
        <li class="active">{{ $route.meta.title }}</li>
    </ol>
</template>

<script>
export default {
    
}
</script>

Penjelasan: $route.meta.title akan mengakses attribute meta.title dari route yang telah didefinisikan pada file router.js, sehingga value yang akan ditampilkan tergantung route yang sedang diakses.

Kembali ke tujuan utama kita, buka file Outlet.vue dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <router-link :to="{ name: 'outlets.add' }" 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">
                <b-table striped hover bordered :items="outlets.data" :fields="fields" show-empty>
                    <template slot="status" slot-scope="row">
                        <span class="label label-success" v-if="row.item.status == 1">Active</span>
                        <span class="label label-default" v-else>Inactive</span>
                    </template>
                    <template slot="actions" slot-scope="row">
                        <router-link :to="{ name: 'outlets.edit', params: {id: row.item.code} }" class="btn btn-warning btn-sm"><i class="fa fa-pencil"></i></router-link>
                        <button class="btn btn-danger btn-sm" @click="deleteOutlet(row.item.id)"><i class="fa fa-trash"></i></button>
                    </template>
                </b-table>

                <div class="row">
                    <div class="col-md-6">
                        <p v-if="outlets.data"><i class="fa fa-bars"></i> {{ outlets.data.length }} item dari {{ outlets.meta.total }} total data</p>
                    </div>
                    <div class="col-md-6">
                        <div class="pull-right">
                            <b-pagination
                                v-model="page"
                                :total-rows="outlets.meta.total"
                                :per-page="outlets.meta.per_page"
                                aria-controls="outlets"
                                v-if="outlets.data && outlets.data.length > 0"
                                ></b-pagination>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

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

export default {
    name: 'DataOutlet',
    created() {
        //SEBELUM COMPONENT DI-LOAD, REQUEST DATA DARI SERVER
        this.getOutlets()
    },
    data() {
        return {
            //FIELD UNTUK B-TABLE, PASTIKAN KEY NYA SESUAI DENGAN FIELD DATABASE
            //AGAR OTOMATIS DI-RENDER
            fields: [
                { key: 'code', label: 'Kode Outlet' },
                { key: 'name', label: 'Nama Outlet' },
                { key: 'address', label: 'Alamat' },
                { key: 'phone', label: 'Telp' },
                { key: 'status', label: 'Status' },
                { key: 'actions', label: 'Aksi' }
            ],
            search: ''
        }
    },
    computed: {
        //MENGAMBIL DATA OUTLETS DARI STATE OUTLETS
        ...mapState('outlet', {
            outlets: state => state.outlets
        }),
        page: {
            get() {
                //MENGAMBIL VALUE PAGE DARI VUEX MODULE outlet
                return this.$store.state.outlet.page
            },
            set(val) {
                //APABILA TERJADI PERUBAHAN VALUE DARI PAGE, MAKA STATE PAGE
                //DI VUEX JUGA AKAN DIUBAH
                this.$store.commit('outlet/SET_PAGE', val)
            }
        }
    },
    watch: {
        page() {
            //APABILA VALUE DARI PAGE BERUBAH, MAKA AKAN MEMINTA DATA DARI SERVER
            this.getOutlets()
        },
        search() {
            //APABILA VALUE DARI SEARCH BERUBAH MAKA AKAN MEMINTA DATA
            //SESUAI DENGAN DATA YANG SEDANG DICARI
            this.getOutlets(this.search)
        }
    },
    methods: {
        //MENGAMBIL FUNGSI DARI VUEX MODULE outlet
        ...mapActions('outlet', ['getOutlets', 'removeOutlet']),
        //KETIKA TOMBOL HAPUS DICLICK, MAKA AKAN MENJALANKAN METHOD INI
        deleteOutlet(id) {
            //AKAN MENAMPILKAN JENDELA KONFIRMASI
            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) => {
                //JIKA DISETUJUI
                if (result.value) {
                    //MAKA FUNGSI removeOutlet AKAN DIJALANKAN
                    this.removeOutlet(id)
                }
            })
        }
    }
}
</script>

Penjelasan: Custom tag <b-table> merupakan component yang telah disediakan oleh Bootstrap Vue. Dimana component ini membutuhkan dua props yakni items dan fields. Items berisi data outlets yang akan di-render ke dalam table, sedangkan fields akan menjadi header dari table tersebut. Karena response yang akan didapatkan dari dari database untuk status hanyalah angka 0 dan 1, maka kita membuat custom rendering dari component b-table menggunakan tag <template>. Adapun pagination akan ditampilkan apabila total data lebih besar dari 0.

Sebelum melangkah ke-step selanjutnya, install beberapa library yang dibutuhkan satu persatu dengan command:

npm i vue bootstrap-vue bootstrap
npm install -S vue-sweetalert2

Kemudian buka file app.js dan tambahkan line berikut:

import BootstrapVue from 'bootstrap-vue'
import VueSweetalert2 from 'vue-sweetalert2'

Vue.use(VueSweetalert2)
Vue.use(BootstrapVue)

import 'bootstrap-vue/dist/bootstrap-vue.css'

Note: Fungsi diatas agar Bootstrap Vue dan Sweetalert2 data digunakan secara global, sehingga pada component lainnya tidak perlu di-import satu persatu.

Step selanjutnya adalah menyediakan methods dan state yang telah dipanggil dari file Outlet.vue, oleh karenanya buat file outlet.js di dalam folder stores dan tambahkan code:

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

const state = () => ({
    outlets: [], //UNTUK MENAMPUNG DATA OUTLETS YANG DIDAPATKAN DARI DATABASE
    
    //UNTUK MENAMPUNG VALUE DARI FORM INPUTAN NANTINYA
    //STATE INI AKAN DIGUNAKAN PADA FORM ADD OUTLET YANG AKAN DIBAHAS KEMUDIAN
    outlet: {
        code: '',
        name: '',
        status: false,
        address: '',
        phone: ''
    },
    page: 1 //UNTUK MENCATAT PAGE PAGINATE YANG SEDANG DIAKSES
})

const mutations = {
    //MEMASUKKAN DATA KE STATE OUTLETS
    ASSIGN_DATA(state, payload) {
        state.outlets = payload
    },
    //MENGUBAH DATA STATE PAGE
    SET_PAGE(state, payload) {
        state.page = payload
    },
    //MENGUBAH DATA STATE OUTLET
    ASSIGN_FORM(state, payload) {
        state.outlet = {
            code: payload.code,
            name: payload.name,
            status: payload.status,
            address: payload.address,
            phone: payload.phone
        }
    },
    //ME-RESET STATE OUTLET MENJADI KOSONG
    CLEAR_FORM(state) {
        state.outlet = {
            code: '',
            name: '',
            status: false,
            address: '',
            phone: ''
        }
    }
}

const actions = {
    //FUNGSI INI UNTUK MELAKUKAN REQUEST DATA OUTLET DARI SERVER
    getOutlets({ commit, state }, payload) {
        //MENGECEK PAYLOAD ADA ATAU TIDAK
        let search = typeof payload != 'undefined' ? payload:''
        return new Promise((resolve, reject) => {
            //REQUEST DATA DENGAN ENDPOINT /OUTLETS
            $axios.get(`/outlets?page=${state.page}&q=${search}`)
            .then((response) => {
                //SIMPAN DATA KE STATE MELALUI MUTATIONS
                commit('ASSIGN_DATA', response.data)
                resolve(response.data)
            })
        })
    },
    //FUNGSI UNTUK MENAMBAHKAN DATA BARU
    submitOutlet({ dispatch, commit, state }) {
        return new Promise((resolve, reject) => {
            //MENGIRIMKAN PERMINTAAN KE SERVER DAN MELAMPIRKAN DATA YANG AKAN DISIMPAN
            //DARI STATE OUTLET
            $axios.post(`/outlets`, state.outlet)
            .then((response) => {
                //APABILA BERHASIL KITA MELAKUKAN REQUEST LAGI
                //UNTUK MENGAMBIL DATA TERBARU
                dispatch('getOutlets').then(() => {
                    resolve(response.data)
                })
            })
            .catch((error) => {
                //APABILA TERJADI ERROR VALIDASI
                //DIMANA LARAVEL MENGGUNAKAN CODE 422
                if (error.response.status == 422) {
                    //MAKA LIST ERROR AKAN DIASSIGN KE STATE ERRORS
                    commit('SET_ERRORS', error.response.data.errors, { root: true })
                }
            })
        })
    },
    //UNTUK MENGAMBIL SINGLE DATA DARI SERVER BERDASARKAN CODE OUTLET
    editOutlet({ commit }, payload) {
        return new Promise((resolve, reject) => {
            //MELAKUKAN REQUEST DENGAN MENGIRIMKAN CODE OUTLET DI URL
            $axios.get(`/outlets/${payload}/edit`)
            .then((response) => {
                //APABIL BERHASIL, DI ASSIGN KE FORM
                commit('ASSIGN_FORM', response.data.data)
                resolve(response.data)
            })
        })
    },
    //UNTUK MENGUPDATE DATA BERDASARKAN CODE YANG SEDANG DIEDIT
    updateOutlet({ state, commit }, payload) {
        return new Promise((resolve, reject) => {
            //MELAKUKAN REQUEST DENGAN MENGIRIMKAN CODE DIURL
            //DAN MENGIRIMKAN DATA TERBARU YANG TELAH DIEDIT
            //MELALUI STATE OUTLET
            $axios.put(`/outlets/${payload}`, state.outlet)
            .then((response) => {
                //FORM DIBERSIHKAN
                commit('CLEAR_FORM')
                resolve(response.data)
            })
        })
    } ,
    //MENGHAPUS DATA 
    removeOutlet({ dispatch }, payload) {
        return new Promise((resolve, reject) => {
            //MENGIRIM PERMINTAAN KE SERVER UNTUK MENGHAPUS DATA
            //DENGAN METHOD DELETE DAN ID OUTLET DI URL
            $axios.delete(`/outlets/${payload}`)
            .then((response) => {
                //APABILA BERHASIL, FETCH DATA TERBARU DARI SERVER
                dispatch('getOutlets').then(() => resolve())
            })
        })
    }
}

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

Note: Ada beberapa fungsi yang belum digunakan, seperti submitOutlet, editOutlet, dan lain lain. Karena akan dikerjakan pada artikel yang sama, maka saya masukkan semua fungsinya untuk menghindari pemenggalan code supaya teman-teman pembaca tidak bingung.

Tentu saja module outlet.js diatas belum berfungsi karena kita belum mendefinisikannya pada root Vuex sebagai bagian module yang bisa digunakan. Buka file store.js, kemudian lakukan modifikasi beberapa bagian berikut:

//[.. CODE SEBELUMNYA ..]

modules: {
    auth,
    outlet //TAMBAHKAN LINE INI
},
  
//[.. CODE SEBELUMNYA ..]

Jangan lupa untuk mengimpan module outlet dengan menambahkan line berikut:

import outlet from './stores/outlet.js'

Saya melakukan sedikit kesalahan pada artikel sebelumnya, buka file api.js dan modifikasi line berikut menjadi:

headers: {
    Authorization: localStorage.getItem('token') != 'null' ? 'Bearer ' + localStorage.getItem('token'):'',
    'Content-Type': 'application/json'
}

Note: Hanya menghapus fungsi JSON.stringify() karena fungsi ini secara otomatis menambahkan double quote "" pada token yang didapatkan.

Sebelum berpindah kesisi backend, modifikasi file Header.vue dengan menambahkan menu navigasi untuk outlets. Sorot baris code berikut dan modifikasi dua line:

<!-- CODE SEBELUMNYA -->
<ul class="nav navbar-nav">
    <li><router-link to="/">Home <span class="sr-only">(current)</span></router-link></li>
    <li><router-link :to="{ name: 'outlets.data' }">Outlets</router-link></li>
</ul>
<!-- CODE SETELAHNYA -->

Dari sisi backend, buat controller untuk meng-handle logic dari outlets, pada command line jalankan command:

php artisan make:controller API/OutletController

Buka file OutletController dan tambahkan method index():

public function index()
{
    $outlets = Outlet::orderBy('created_at', 'DESC');
    if (request()->q != '') {
        $outlets = $outlets->where('name', 'LIKE', '%' . request()->q . '%');
    }
    return new OutletCollection($outlets->paginate(10));
}

Penjelasan: Line-1 untuk mengambil data outlets yang telah di-sortir berdasarkan created_at dengan urutan teratas adalah data terbaru. Line selanjutnya apabila q tidak kosong atau ada parameter pencarian yang dikirimkan, maka kita menambahkan query untuk mem-filter data berdasarkan name dari outlets. Line selanjutnya kita akan menggunakan API resources dari Laravel untuk men-format data dari hasil query yang telah dilakukan, dimana data yang diambil akan kita batas hanya 10 data dalam sekali query menggunakan fungsi paginate().

Buat class OutletCollection dengan command:

php artisan make:resource OutletCollection

Buka file OutletCollection.php yang berada didalam folder app\Http\Resources dan modifikasi menjadi:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

Kembali ke OutletController.php, gunakan use statement untuk model Outlet dan OutletCollection:

use App\Http\Resources\OutletCollection;
use App\Outlet;

Mari kita buat route API dimana route ini hanya bisa diakses apabila request yang terima memiliki token yang valid. Buka file routes/api.php dan tambahkan code:

Route::group(['middleware' => 'auth:api'], function() {
    Route::resource('/outlets', 'API\OutletController')->except(['show']);
});

Note: Middleware auth:api secara otomatis akan mem-protect route yang ada didalam nya dengan mengharuskan menggunakan token yang valid.

Pastikan sebelum mengaksesnya pada browser, jalankan command:

npm run watch

Adapun hasil yang akan diproleh seperti berikut.

list data outlet aplikasi laundry laravel

Add New Outlet

Tugas selanjutnya adalah memodifikasi file Add.vue dimana file ini akan meng-handle fungsi untuk menyimpan data baru ke database. Buka file Add.vue dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Add New Outlet</h3>
            </div>
            <div class="panel-body">
                <outlet-form></outlet-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 { mapActions, mapState } from 'vuex'
    import FormOutlets from './Form.vue'
    export default {
        name: 'AddOutlet',
        data() {
            return {
                
            }
        },
        methods: {
            ...mapActions('outlet', ['submitOutlet']), //PANGGIL ACTIONS submitOutlet
            submit() {
                //KETIKA TOMBOL DITEKAN MAKA FUNGSI INI AKAN DIJALANKAN
                //DIMANA FUNGSI INI TELAH DIBUAT PADA SESI SEBELUMNYA
                this.submitOutlet().then(() => {
                    //APABILA BERHASIL MAKA AKAN DI-DIRECT KE HALAMAN /outlets
                    this.$router.push({ name: 'outlets.data' })
                })
            }
        },
        components: {
            'outlet-form': FormOutlets
        }
    }
</script>

Penjelasan: Tag <outlet-form></outlet-form> berasal dari file Form.vue dimana file ini memuat input-an untuk data outlets. Karena Add dan Edit memiliki kesamaan, maka inputan-nya kita satukan ke dalam sebuah file agar tidak menuliskan code yang sama secara berulang.

Buat file Form.vue di dalam folder pages/outlets dan tambahkan code:

<template>
    <div>
        <div class="form-group" :class="{ 'has-error': errors.code }">
            <label for="">Kode Outlet</label>
            <input type="text" class="form-control" v-model="outlet.code" :readonly="$route.name == 'outlets.edit'">
            <p class="text-danger" v-if="errors.code">{{ errors.code[0] }}</p>
        </div>
        <div class="form-group" :class="{ 'has-error': errors.name }">
            <label for="">Nama Outlet</label>
            <input type="text" class="form-control" v-model="outlet.name">
            <p class="text-danger" v-if="errors.name">{{ errors.name[0] }}</p>
        </div>
        <div class="form-group" :class="{ 'has-error': errors.address }">
            <label for="">Alamat</label>
            <textarea cols="5" rows="5" class="form-control" v-model="outlet.address"></textarea>
            <p class="text-danger" v-if="errors.address">{{ errors.address[0] }}</p>
        </div>
        <div class="form-group" :class="{ 'has-error': errors.phone }">
            <label for="">No Telp</label>
            <input type="text" class="form-control" v-model="outlet.phone">
            <p class="text-danger" v-if="errors.phone">{{ errors.phone[0] }}</p>
        </div>
        <div class="form-group">
            <input type="checkbox" v-model="outlet.status">
            <label for=""> Set Active</label>
        </div>
    </div>
</template>

<script>
import { mapState, mapMutations } from 'vuex'
export default {
    name: 'FormOutlets',
    computed: {
        ...mapState(['errors']), //MENGAMBIL STATE ERRORS
        ...mapState('outlet', {
            outlet: state => state.outlet //MENGAMBIL STATE OUTLET
        })
    },
    methods: {
        ...mapMutations('outlet', ['CLEAR_FORM']), //PANGGIL MUTATIONS CLEAR_FORM
    },
    //KETIKA PAGE INI DITINGGALKAN MAKA 
    destroyed() {
        //FORM DI BERSIHKAN
        this.CLEAR_FORM()
    }
}
</script>

Penjelasan: Pada form diatas, kita melakukan pengecekan error validasi yang dihasilkan oleh Laravel. Misalnya, apabila errors.name ada maka class has-error akan ditambahkan. Selain itu tag p dengan class text-danger yang berisi pesan error akan ditampilkan.

Berpindah ke sisi backend, buka file OutletController.php dan tambahkan method store()

public function store(Request $request)
{
    $this->validate($request, [
        'code' => 'required|unique:outlets,code',
        'name' => 'required|string|max:100',
        'address' => 'required|string',
        'phone' => 'required|max:13'
    ]);

    Outlet::create($request->all());
    return response()->json(['status' => 'success'], 200);
}

Penjelasan: Bagian pertama kita melakukan validasi dimana 4 field yang tertera wajib diisi, dimana code harus bersifat unik, name dan phone memiliki limit maksimal karakter yang diperbolehkan. Selanjutnya kita menyimpan data ke database menggunakan mass asignment, dimana semua data yang dikirim dari client akan diambil menggunakan method all(). Terakhir memberikan response berhasil.

Kita tidak perlu mendefinisikan route API dari Laravel lagi karena telah di-cover oleh Route::resouce(), maka yang perlu dilakukan selanjutnya adalah mengizinkan field apa saja yang diperboleh untuk menyimpan data, tentu saja saya akan menggunakan property guarded untuk mengizinkan semua field yang ada. Buka file Outlet.php dan modifikasi menjadi:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Outlet extends Model
{
    protected $guarded = [];
}

aplikasi laundry laravel form add outlets

Edit Outlet

File terakhir yang membutuhkan perhatian adalah Edit.vue, file ini berfungsi untuk meng-handle input-an edit data outlet. Buka file tersebut dan tambahkan code:

<template>
    <div class="col-md-12">
        <div class="panel">
            <div class="panel-heading">
                <h3 class="panel-title">Edit Outlet</h3>
            </div>
            <div class="panel-body">
                <outlet-form></outlet-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 { mapActions, mapState } from 'vuex'
    import FormOutlets from './Form.vue'
    export default {
        name: 'EditOutlet',
        created() {
            //SEBELUM COMPONENT DI-RENDER KITA MELAKUKAN REQUEST KESERVER
            //BERDASARKAN CODE YANG ADA DI URL
            this.editOutlet(this.$route.params.id)
        },
        methods: {
            ...mapActions('outlet', ['editOutlet', 'updateOutlet']),
            submit() {
                //KETIKA TOMBOL UPDATE DI MAKA AKAN MENGIRIMKAN PERMINTAAN
                //UNTUK MENGUBAH DATA BERDASARKAN CODE YANG DIKIRIMKAN
                this.updateOutlet(this.$route.params.id).then(() => {
                    //APABILA BERHASIL MAKA AKAN DI-DIRECT KEMBALI
                    //KE HALAMAN /outlets
                    this.$router.push({ name: 'outlets.data' })
                })
            }
        },
        components: {
            'outlet-form': FormOutlets
        },
    }
</script>

Note: Sama dengan Add.vue kita hanya perlu menggunakan component Form.vue dengan tag <outlet-form>.

Kembali kesisi backend, buka file OutletController.php dan tambahkan kedua method berikut:

public function edit($id)
{
    $outlet = Outlet::whereCode($id)->first();
    return response()->json(['status' => 'success', 'data' => $outlet], 200);
}

public function update(Request $request, $id)
{
    $this->validate($request, [
        'code' => 'required|exists:outlets,code',
        'name' => 'required|string|max:100',
        'address' => 'required|string',
        'phone' => 'required|max:13'
    ]);

    $outlet = Outlet::whereCode($id)->first();
    $outlet->update($request->except('code'));
    return response()->json(['status' => 'success'], 200);
}

Penjelasan: Pada method edit() hanya berfungsi untuk mengambil single data berdasarkan code dan mengirimkannya kembali ke client. Pada method update() kita melakukan validasi dan mengambil data berdasarkan code kemudian mengubah data tersebut dengan pengecualian bahwa code tidak diubah.

aplikasi laundry laravel edit outlets

Delete Outlet

Kita tidak akan melakukan perubahan pada sisi client dalam hal ini adalah Vue.js, karena fungsi untuk mengirimkan permintaan hapus data sudah dikerjakan pada bagian Manage Outlets. Hal yang akan dilakukan pada bagian ini adalah membuat API-nya untuk memproses permintaan tersebut. Buka file OutletController.php dan tambahkan method:

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

Penjelasan: Line pertama mengambil data berdasarkan id dan kemudian pada line selanjutnya menghapus data tersebut menggunakan method delete().

aplikasi laundry laravel delete data

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #1: Schema Database

Update Issue

Jadi beberapa dari teman-teman mengalami masalah dimana status dan aksi tidak tampil pada list outlets (hal ini sebenarnya berlaku untuk semua yang menggunakan custom template). Mengapa demikian? Karena perbedaan versi Bootstrap-vue, dimana update terbaru Bootstrap-vue pendekatannya berbeda untuk penggunaan custom template. Lakukan modifikasi bagian berikut

//UBAH BAGIAN INI
slot="actions" slot-scope="row" 

//MENJADI
v-slot:cell(actions)="row"

Note: Sesuaikan key actions berdasarkan field yang kamu punya. 

Kesimpulan

CRUD menjadi bagian konsep yang harus dipahami karena hampir semua sistem memiliki fungsi CRUD meski bagian dari CRUD digunakan, pada serial ini kita telah belajar bagaimana memanipulasi database atau yang dikenal dengan istilah CRUD menggunakan Laravel dan Vue.js. Selain itu kita juga telah belajar bagaimana membuat validasi dan meng-handle validasi dari Laravel menggunakan Vue.js, hal lainnya adalah mengirimkan protected request untuk mengambil data dari database.

Adapun dokumentasi code dari serial ini dapat dilihat di Github.

Category:
Share:

Comments