Aplikasi E-Commerce Laravel 6 #14: Kelola Pesanan (Admin) & Broadcast Resi

Aplikasi E-Commerce Laravel 6 #14: Kelola Pesanan (Admin) & Broadcast Resi

Pendahuluan

Interaksi antar pengguna dan pemilik adalah dengan adanya timbal balik dari dua sisi, dimana sisi user atau pelanggan sudah melakukan tugasnya yakni membuat pesanan, maka dari sisi admin harus merespon pesanan tersebut melalui halaman Kelola Pesanan E-Commerce.

Setiap data pesanan yang valid dan membutuhkan bukti pengiriman akan ditanggapi oleh admin dengan membuat broadcast data nomor resi secara otomatis melalui email ketika admin memasukkan nomor resi pada halaman detail pesanan.

Baca Juga: Aplikasi E-Commerce Laravel 6 #13: Membuat Faktur PDF

[ISSUE] Fix Repeat Order Customers

Dalam seri sebelumnya, pembaca DaengWeb menemukan sebuah bug dimana customer yang sudah pernah melakukan transaksi & ingin membuat transaksi baru mengalami kendala dalam validasinya dimana customer tersebut diminta untuk login kembali padahal statusnya sudah login.

Hal ini disebabkan karena terjadinya kesalahan dalam pengecekan authentication, dimana seharusnya menggunakan guard customer tapi guard tersebut tidak diterapkan. Untuk mengatasi masalah ini, buka file Ecommerce/CartController.php dan modifikasi method berikut

public function processCheckout(Request $request)
    {
        $this->validate($request, [
            'customer_name' => 'required|string|max:100',
            'customer_phone' => 'required',
            'email' => 'required|email',
            'customer_address' => 'required|string',
            'province_id' => 'required|exists:provinces,id',
            'city_id' => 'required|exists:cities,id',
            'district_id' => 'required|exists:districts,id'
        ]);

        DB::beginTransaction();
        try {
            $customer = Customer::where('email', $request->email)->first();

            //LAKUKAN PERUBAHAN PADA BAGIAN INI
            if (!auth()->guard('customer')->check() && $customer) {
                return redirect()->back()->with(['error' => 'Silahkan Login Terlebih Dahulu']);
            }

            $carts = $this->getCarts();
            $subtotal = collect($carts)->sum(function($q) {
                return $q['qty'] * $q['product_price'];
            });

            //UNTUK MENGHINDARI DUPLICATE CUSTOMER, MASUKKAN QUERY UNTUK MENAMBAHKAN CUSTOMER BARU
            //SEBENARNYA VALIDASINYA BISA DIMASUKKAN PADA METHOD VALIDATION DIATAS, TAPI TIDAK MENGAPA UNTUK MENCOBA CARA BERBEDA
            if (!auth()->guard('customer')->check()) {
                $password = Str::random(8);
                $customer = Customer::create([
                    'name' => $request->customer_name,
                    'email' => $request->email,
                    'password' => $password,
                    'phone_number' => $request->customer_phone,
                    'address' => $request->customer_address,
                    'district_id' => $request->district_id,
                    'activate_token' => Str::random(30),
                    'status' => false
                ]);
            }

            $order = Order::create([
                'invoice' => Str::random(4) . '-' . time(),
                'customer_id' => $customer->id,
                'customer_name' => $customer->name,
                'customer_phone' => $request->customer_phone,
                'customer_address' => $request->customer_address,
                'district_id' => $request->district_id,
                'subtotal' => $subtotal
            ]);

            foreach ($carts as $row) {
                $product = Product::find($row['product_id']);
                OrderDetail::create([
                    'order_id' => $order->id,
                    'product_id' => $row['product_id'],
                    'price' => $row['product_price'],
                    'qty' => $row['qty'],
                    'weight' => $product->weight
                ]);
            }

            DB::commit();

            $carts = [];
            $cookie = cookie('dw-carts', json_encode($carts), 2880);

            //EMAILNYA JUGA UNTUK CUSTOMER BARU
            if (!auth()->guard('customer')->check()) {
                Mail::to($request->email)->send(new CustomerRegisterMail($customer, $password));
            }
            return redirect(route('front.finish_checkout', $order->invoice))->cookie($cookie);
        } catch (\Exception $e) {
            DB::rollback();
            return redirect()->back()->with(['error' => $e->getMessage()]);
        }
    }

Perubahan kecil selanjutnya adalah apabila customer sudah login, maka field email ketika checkout pesanan akan otomatis terisi dan readonly. Buka file resources/views/ecommerce/checkout.blade.php dan modifikasi pada tag inputan email menjadi.

<div class="col-md-6 form-group p_star">
    <label for="">Email</label>
    <input type="email" class="form-control" id="email" name="email" value="{{ auth()->guard('customer')->user()->email }}" required {{ auth()->guard('customer')->check() ? 'readonly':'' }}>
    <p class="text-danger">{{ $errors->first('email') }}</p>
</div>

Fitur Kelola Pesanan

Administrator memiliki kendali dalam melihat dan mengelola semua pesanan yang masuk. Skenario yang akan digunakan adalah semua pesanan tersebut akan ditampilkan dengan urutan pesanan terbaru akan berada pada posisi paling atas di halaman pertama. Selanjutnya terdapat fitur untuk mencari pesanan berdasarkan invoice, alamat dan nama pelanggan. Lain hanya adalah fitur untuk mem-filter berdasarkan status pesanan.

Buat controller baru dengan command

php artisan make:controller OrderController

Buka file OrderController yang baru dan tambahkan method index()

public function index()
{
    //QUERY UNTUK MENGAMBIL SEMUA PESANAN DAN LOAD DATA YANG BERELASI MENGGUNAKAN EAGER LOADING
    //DAN URUTANKAN BERDASARKAN CREATED_AT
    $orders = Order::with(['customer.district.city.province'])
        ->orderBy('created_at', 'DESC');

    //JIKA Q UNTUK PENCARIAN TIDAK KOSONG
    if (request()->q != '') {
        //MAKA DIBUAT QUERY UNTUK MENCARI DATA BERDASARKAN NAMA, INVOICE DAN ALAMAT
        $orders = $orders->where(function($q) {
            $q->where('customer_name', 'LIKE', '%' . request()->q . '%')
            ->orWhere('invoice', 'LIKE', '%' . request()->q . '%')
            ->orWhere('customer_address', 'LIKE', '%' . request()->q . '%');
        });
    }

    //JIKA STATUS TIDAK KOSONG 
    if (request()->status != '') {
        //MAKA DATA DIFILTER BERDASARKAN STATUS
        $orders = $orders->where('status', request()->status);
    }
    $orders = $orders->paginate(10); //LOAD DATA PER 10 DATA
    return view('orders.index', compact('orders')); //LOAD VIEW INDEX DAN PASSING DATA TERSEBUT
}

Jangan lupa menambahkan use statement di dalam file yang sama.

use App\Order;

Buat file baru dengan nama index.blade.php di dalam folder resources/views/orders dan masukkan kerangka HTML berikut

@extends('layouts.admin')

@section('title')
    <title>Daftar Pesanan</title>
@endsection

@section('content')
<main class="main">
    <ol class="breadcrumb">
        <li class="breadcrumb-item">Home</li>
        <li class="breadcrumb-item active">Orders</li>
    </ol>
    <div class="container-fluid">
        <div class="animated fadeIn">
            <div class="row">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-header">
                            <h4 class="card-title">
                                Daftar Pesanan
                            </h4>
                        </div>
                        <div class="card-body">
                            @if (session('success'))
                                <div class="alert alert-success">{{ session('success') }}</div>
                            @endif

                            @if (session('error'))
                                <div class="alert alert-danger">{{ session('error') }}</div>
                            @endif

                          	<!-- FORM UNTUK FILTER DAN PENCARIAN -->
                            <form action="{{ route('orders.index') }}" method="get">
                                <div class="input-group mb-3 col-md-6 float-right">
                                    <select name="status" class="form-control mr-3">
                                        <option value="">Pilih Status</option>
                                        <option value="0">Baru</option>
                                        <option value="1">Confirm</option>
                                        <option value="2">Proses</option>
                                        <option value="3">Dikirim</option>
                                        <option value="4">Selesai</option>
                                    </select>
                                    <input type="text" name="q" class="form-control" placeholder="Cari..." value="{{ request()->q }}">
                                    <div class="input-group-append">
                                        <button class="btn btn-secondary" type="submit">Cari</button>
                                    </div>
                                </div>
                            </form>
                            <!-- FORM UNTUK FILTER DAN PENCARIAN -->
                          
                          	<!-- TABLE UNTUK MENAMPILKAN DATA ORDER -->
                            <div class="table-responsive">
                                <table class="table table-hover table-bordered">
                                    <thead>
                                        <tr>
                                            <th>InvoiceID</th>
                                            <th>Pelanggan</th>
                                            <th>Subtotal</th>
                                            <th>Tanggal</th>
                                            <th>Status</th>
                                            <th>Aksi</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        @forelse ($orders as $row)
                                        <tr>
                                            <td><strong>{{ $row->invoice }}</strong></td>
                                            <td>
                                                <strong>{{ $row->customer_name }}</strong><br>
                                                <label><strong>Telp:</strong> {{ $row->customer_phone }}</label><br>
                                                <label><strong>Alamat:</strong> {{ $row->customer_address }} {{ $row->customer->district->name }} - {{  $row->customer->district->city->name}}, {{ $row->customer->district->city->province->name }}</label>
                                            </td>
                                            <td>Rp {{ number_format($row->subtotal) }}</td>
                                            <td>{{ $row->created_at->format('d-m-Y') }}</td>
                                            <td>{!! $row->status_label !!}</td>
                                            <td>
                                                <form action="{{ route('orders.destroy', $row->id) }}" method="post">
                                                    @csrf
                                                    @method('DELETE')
                                                    <a href="{{ route('orders.view', $row->invoice) }}" class="btn btn-warning btn-sm">Lihat</a>
                                                    <button class="btn btn-danger btn-sm">Hapus</button>
                                                </form>
                                            </td>
                                        </tr>
                                        @empty
                                        <tr>
                                            <td colspan="6" class="text-center">Tidak ada data</td>
                                        </tr>
                                        @endforelse
                                    </tbody>
                                </table>
                            </div>
                            {!! $orders->links() !!}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>
@endsection

Definisikan relationships dari orders ke table customers, buka file Order.php dan tambahkan method

public function customer()
{
    return $this->belongsTo(Customer::class);
}

Tugas kita selanjutnya adalah mendefinisikan routing, buka file routes/web.php dan tambahkan code berikut tepat di dalam route group yang menggunakan prefix administrator

Route::group(['prefix' => 'orders'], function() {
    Route::get('/', 'OrderController@index')->name('orders.index');
    
    //SEMUA ROUTE BARU SEPANJANG ARTIKEL INI AKAN DISIMPAN DI DALAM BLOCK CODE INI
});

Buat navigasi dari sidebar menu, buka file sidebar.blade.php di dalam folder resources/views/layouts/module dan tambahkan tag berikut di bawah menu Product.

<li class="nav-item">
    <a class="nav-link" href="{{ route('orders.index') }}">
        <i class="nav-icon icon-drop"></i> Pesanan
    </a>
</li>

Fitur Hapus Pesanan

Fitur selanjutnya yang harus disediakan adalah fungsi untuk menghapus pesanan pelanggan, karena tombolnya sudah tersedia dan mengarah ke route orders.destroy, maka tugas kita adalah meng-handle request tersebut. Buka file OrderController.php dan tambahkan method

public function destroy($id)
{
    $order = Order::find($id);
    $order->details()->delete();
    $order->payment()->delete();
    $order->delete();
    return redirect(route('orders.index'));
}

Terakhir definisikan routing-nya dengan menambahkan code

Route::delete('/{id}', 'OrderController@destroy')->name('orders.destroy');

Fitur Detail Pesanan

Ketika admin ingin melihat pesanan secara detail maka diperlukan sebuah halaman untuk menampilkan data tersebut dan di halaman ini juga admin memiliki beberapa dua kemampuan, pertama adalah menerima pembayaran yang sudah dikonfirmasi oleh customer dan yang kedua adalah meng-input resi dan secara otomatis akan dikirimkan ke email pelanggan terkait.

Langkah awal kita buat tampilannya dulu, buka file OrderContorller.php dan tambahkan method

public function view($invoice)
{
    $order = Order::with(['customer.district.city.province', 'payment', 'details.product'])->where('invoice', $invoice)->first();
    return view('orders.view', compact('order'));
}

Kemudian buat file view.blade.php di dalam folder orders dimana file ini akan ditempatkan di dalam file index.blade.php yang sudah dibuat sebelumnya. dan tambahkan code

@extends('layouts.admin')

@section('title')
    <title>Detail pesanan</title>
@endsection

@section('content')
<main class="main">
    <ol class="breadcrumb">
        <li class="breadcrumb-item">Home</li>
        <li class="breadcrumb-item active">View Order</li>
    </ol>
    <div class="container-fluid">
        <div class="animated fadeIn">
            <div class="row">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-header">
                            <h4 class="card-title">
                                Detail pesanan

                                <!-- TOMBOL UNTUK MENERIMA PEMBAYARAN -->
                                <div class="float-right">
                                    <!-- TOMBOL INI HANYA TAMPIL JIKA STATUSNYA 1 DARI ORDER DAN 0 DARI PEMBAYARAN -->
                                    @if ($order->status == 1 && $order->payment->status == 0)
                                    <a href="{{ route('orders.approve_payment', $order->invoice) }}" class="btn btn-primary btn-sm">Terima Pembayaran</a>
                                    @endif
                                </div>
                            </h4>
                        </div>
                        <div class="card-body">
                            <div class="row">
                              
                                <!-- BLOCK UNTUK MENAMPILKAN DATA PELANGGAN -->
                                <div class="col-md-6">
                                    <h4>Detail Pelanggan</h4>
                                    <table class="table table-bordered">
                                        <tr>
                                            <th width="30%">Nama Pelanggan</th>
                                            <td>{{ $order->customer_name }}</td>
                                        </tr>
                                        <tr>
                                            <th>Telp</th>
                                            <td>{{ $order->customer_phone }}</td>
                                        </tr>
                                        <tr>
                                            <th>Alamat</th>
                                            <td>{{ $order->customer_address }} {{ $order->customer->district->name }} - {{  $order->customer->district->city->name}}, {{ $order->customer->district->city->province->name }}</td>
                                        </tr>
                                        <tr>
                                            <th>Order Status</th>
                                            <td>{!! $order->status_label !!}</td>
                                        </tr>
                                        <!-- FORM INPUT RESI HANYA AKAN TAMPIL JIKA STATUS LEBIH BESAR 1 -->
                                        @if ($order->status > 1)
                                        <tr>
                                            <th>Nomor Resi</th>
                                            <td>
                                                @if ($order->status == 2)
                                                <form action="{{ route('orders.shipping') }}" method="post">
                                                    @csrf
                                                    <div class="input-group">
                                                        <input type="hidden" name="order_id" value="{{ $order->id }}">
                                                        <input type="text" name="tracking_number" placeholder="Masukkan Nomor Resi" class="form-control" required>
                                                        <div class="input-group-append">
                                                            <button class="btn btn-secondary" type="submit">Kirim</button>
                                                        </div>
                                                    </div>
                                                </form>
                                                @else
                                                {{ $order->tracking_number }}
                                                @endif
                                            </td>
                                        </tr>
                                        @endif
                                    </table>
                                </div>
                                <div class="col-md-6">
                                    <h4>Detail Pembayaran</h4>
                                    @if ($order->status != 0)
                                    <table class="table table-bordered">
                                        <tr>
                                            <th width="30%">Nama Pengirim</th>
                                            <td>{{ $order->payment->name }}</td>
                                        </tr>
                                        <tr>
                                            <th>Bank Tujuan</th>
                                            <td>{{ $order->payment->transfer_to }}</td>
                                        </tr>
                                        <tr>
                                            <th>Tanggal Transfer</th>
                                            <td>{{ $order->payment->transfer_date }}</td>
                                        </tr>
                                        <tr>
                                            <th>Bukti Pembayaran</th>
                                            <td><a target="_blank" href="{{ asset('storage/payment/' . $order->payment->proof) }}">Lihat</a></td>
                                        </tr>
                                        <tr>
                                            <th>Status</th>
                                            <td>{!! $order->payment->status_label !!}</td>
                                        </tr>
                                    </table>
                                    @else
                                    <h5 class="text-center">Belum Konfirmasi Pembayaran</h5>
                                    @endif
                                </div>
                                <div class="col-md-12">
                                    <h4>Detail Produk</h4>
                                    <table class="table table-borderd table-hover">
                                        <tr>
                                            <th>Produk</th>
                                            <th>Quantity</th>
                                            <th>Harga</th>
                                            <th>Berat</th>
                                            <th>Subtotal</th>
                                        </tr>
                                        @foreach ($order->details as $row)
                                        <tr>
                                            <td>{{ $row->product->name }}</td>
                                            <td>{{ $row->qty }}</td>
                                            <td>Rp {{ number_format($row->price) }}</td>
                                            <td>{{ $row->weight }} gr</td>
                                            <td>Rp {{ $row->qty * $row->price }}</td>
                                        </tr>
                                        @endforeach
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>
@endsection

Note: Tidak ada yang bisa dijelaskan secara detail dari code di atas karena sudah pernah dibahas sebelumnya, yang berbeda hanya case-nya saja.

Definisikan routing-nya dengan menambahkan code di bawah ini ke dalam file routes/web.php

Route::get('/{invoice}', 'OrderController@view')->name('orders.view');

Ada Accessor baru terkait label dari status pembayaran pelanggan, buka file Payment.php dan tambahkan baris code

protected $appends = ['status_label'];

public function getStatusLabelAttribute()
{
    if ($this->status == 0) {
        return '<span class="badge badge-secondary">Menunggu Konfirmasi</span>';
    }
    return '<span class="badge badge-success">Diterima</span>';
}

Tampilan yang diinginkan sudah tersedia, tugas berikutnya adalah meng-handle request untuk approve pembayaran. Buka file OrderController.php dan tambahkan method

public function acceptPayment($invoice)
{
    //MENGAMBIL DATA CUSTOMER BERDASARKAN INVOICE
    $order = Order::with(['payment'])->where('invoice', $invoice)->first();
    //UBAH STATUS DI TABLE PAYMENTS MELALUI ORDER YANG TERKAIT
    $order->payment()->update(['status' => 1]);
    //UBAH STATUS ORDER MENJADI PROSES
    $order->update(['status' => 2]);
    //REDIRECT KE HALAMAN YANG SAMA.
    return redirect(route('orders.view', $order->invoice));
}

Definisikan routing-nya agar fungsi di atas bisa berjalan, buka file routes/web.php

Route::get('/payment/{invoice}', 'OrderController@acceptPayment')->name('orders.approve_payment');

Fitur Broadcast Resi Pengiriman

Bagian terakhir yang harus diselesaikan adalah meng-handle request untuk mengirimkan resi pengiriman melalui email ke pelanggan terkait. Adapun form untuk meng-input-nya sudah disediakan pada sesi sebelumnya, jadi tugas kita hanya menerima request.

Buka kembali file OrderController.php dan tambahkan method

public function shippingOrder(Request $request)
{
    //MENGAMBIL DATA ORDER BERDASARKAN ID
    $order = Order::with(['customer'])->find($request->order_id);
    //UPDATE DATA ORDER DENGAN MEMASUKKAN NOMOR RESI DAN MENGUBAH STATUS MENJADI DIKIRIM
    $order->update(['tracking_number' => $request->tracking_number, 'status' => 3]);
    //KIRIM EMAIL KE PELANGGAN TERKAIT
    Mail::to($order->customer->email)->send(new OrderMail($order));
    //REDIRECT KEMBALI
    return redirect()->back();
}

Jangan lupa untuk menambahkan use statement di dalam file yang sama

use App\Mail\OrderMail;
use Mail;

Ada dua bagian yang harus diselesaikan, pertama adalah menambahkan field tracking_number ke dalam table orders dan yang kedua adalah men-generate file baru untuk meng-handle pengiriman email.

Generate migration baru dengan command

php artisan make:migration add_field_tracking_number_to_orders_table

Buka file migration yang baru saja di-generate dan modifikasi menjadi

<?php

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

class AddFieldTrackingNumberToOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('orders', function (Blueprint $table) {
            //MENAMBAHKAN FIELD BARU SETELAH FIELD STATUS
            $table->string('tracking_number')->nullable()->after('status');
        });
    }

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

Eksekusi migration di atas dengan command.

php artisan migrate

Generate file baru lagi untuk mailing, pada command line jalankan command

php artisan make:mail OrderMail

Kemudian buka file OrderMail.php dan modifikasi menjadi

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Order;

class OrderMail extends Mailable
{
    use Queueable, SerializesModels;
    protected $order;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    //MEMINTA DATA ORDER
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        //DAN KIRIM EMAIL DENGAN SUBJECT BERIKUT
        //TEMPLATE YANG DIGUNAKAN ADALAH ORDER.BLADE.PHP YANG ADA DI FOLDER EMAILS
        //DAN PASSING DATA ORDER KE FILE ORDER.BLADE.PHP
        return $this->subject('Pesanan Anda Dikirim ' . $this->order->invoice)
            ->view('emails.order')
            ->with([
                'order' => $this->order
            ]);
    }
}

Penuhi template untuk email, buat file baru bernama order.blade.php di dalam folder resources/views/emails dan tambahkan tag berikut

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Pesanan Anda Dikirim {{ $order->invoice }}</title>
</head>
<body>
    <h2>Hai, {{ $order->customer->name }}</h2>
    <p>Terima kasih telah melakukan transaksi pada aplikasi kami, berikut nomor resi dari pesanan anda: <strong>{{ $order->tracking_number }}</strong></p>
</body>
</html>

Bagian penutup adalah mendefinisikan routing-nya, buka file routes/web.php dan tambahkan code

Route::post('/shipping', 'OrderController@shippingOrder')->name('orders.shipping');

Baca Juga: 7 Tips Cara Cepat Belajar Bahasa Pemrograman

Kesimpulan

Mengelola pesanan adalah sebuah fitur penting untuk berinteraksi dengan user, maka dalam seri Membuat Aplikasi E-Commerce Laravel 6 ke-14 ini kita membahas fitur Kelola Pesanan (admin) dan Broadcast resi mengajarkan tentang pengulangan fungsi dengan case yang berbeda.

Semua materi dan fungsi yang digunakan pada pembahasan di atas sudah pernah dijelaskan sebelumnya, maka semoga dengan pengulangan ini kita menjadi lebih baik dalam memahami cara berinteraksi dengan Laravel.

Adapun dokumentasi code dari artike ini bisa dilihat di Github.

Category:
Share:

Comments