Pendahuluan
Lanjutan dari serial sebelumnya, kali ini kita akan membuat sebuah fitur untuk meng-handle invoice, seperti membuat invoice baru dan menampilkan invoice tersebut beserta detail yang telah di-input oleh user. Masih seputaran CRUD, sebab sepanjang serial Membuat aplikasi Invoice Laravel 5.7 adalah "seni" memainkan CRUD sehingga menjadi sebuah aplikasi yang diinginkan.
Kenapa masih disebut bagian dari CRUD? Sebab fitur untuk membuat invoice baru, kita menggunakan fungsi Create. sedangkan fitur untuk menampilkan, invoice yang digunakan adalah fungsi Read. Ada dua point yang akan kita capai pada artikel ini, yakni membuat invoice dari halaman invoice itu sendiri lalu menampilkannya dan atau membuat invoice dari halaman customer.
Baca Juga: Membuat Aplikasi Invoice Laravel 5.7 #1: Generate Database
Create new Invoice
Pada fitur ini, schema-nya adalah user terlebih dahulu memilih customer, lalu di-direct ke halaman selanjutnya untuk menambahkan produk yang akan dimasukkan ke invoice. Ada dua hal yang berperan, pertama fungsi untuk meng-input data ke table invoices
, kemudian ketika menambahkan produk fungsi yang bekerja selanjutnya adalah untuk menyimpan ke table invoice_details
lalu meng-update kembali table invoices
, dalam hal ini field total.
Buat controller InvoiceController
dengan command:
php artisan make:controller InvoiceController
Kemudian tambahkan code berikut:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Customer;
class InvoiceController extends Controller
{
public function create()
{
$customers = Customer::orderBy('created_at', 'DESC')->get();
return view('invoice.create', compact('customers'));
}
}
Kemudian buat view create.blade.php
didalam folder resources/views/invoice
dan masukkan code berikut:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Buat Invoice</h3>
</div>
<div class="card-body">
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form action="{{ route('invoice.store') }}" method="post">
@csrf
<div class="form-group">
<label for="">Customer</label>
<select name="customer_id" class="form-control" required>
<option value="">Pilih</option>
@foreach ($customers as $customer)
<option value="{{ $customer->id }}">{{ $customer->name }} - {{ $customer->email }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm">Buat</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Penjelasan: Ada code yang baru yakni: {{ route('invoice.store') }}
, dimana kita menggunakan helper route() untuk membuat url yang dinamis. Perbedaannya dengan helper url(), ketika path dari url berubah maka path didalam helper url() juga harus disesuaikan. Sedangkan untuk penggunakan helper route(), kita cukup mendefinisikan name dari route tersebut, lalu menggunakan name tersebut. Apabila path url berubah, maka kita tidak perlu menyesuaikan pathnya pada helper route() karena yang digunakan adalah name-nya bukan path-nya.
Selanjutnya definisikan route dengan name invoice.store
, buka file routes/web.php
kemudian tambahkan code berikut:
Route::group(['prefix' => 'invoice'], function() {
//ROUTE UNTUK HALAMAN INVOICE
Route::get('/new', 'InvoiceController@create')->name('invoice.create');
//ROUTE UNTUK MENG-HANDLE DATA YANG DIKIRIM
Route::post('/', 'InvoiceController@save')->name('invoice.store');
});
Pada InvoiceController
tambahkan method save():
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Customer;
use App\Invoice; // ADD USE STATEMENT
class InvoiceController extends Controller
{
// [.. CODE SEBELUMNYA ..]
public function save(Request $request)
{
//VALIDASI
$this->validate($request, [
'customer_id' => 'required|exists:customers,id'
]);
try {
//MENYIMPAN DATA KE TABLE INVOICES
$invoice = Invoice::create([
'customer_id' => $request->customer_id,
'total' => 0
]);
//REDIRECT KE ROUTE invoice.edit DENGAN MENGIRIMKAN PARAMETER ID
return redirect(route('invoice.edit', ['id' => $invoice->id]));
} catch(\Exception $e) {
//JIKA GAGAL REDIRECT BACK KE FORM, DAN MENAMPILKAN ERROR MESSAGE
return redirect()->back()->with(['error' => $e->getMessage()]);
}
}
public function edit($id)
{
$invoice = Invoice::with(['customer', 'detail', 'detail.product'])->find($id);
$products = Product::orderBy('title', 'ASC')->get();
return view('invoice.edit', compact('invoice', 'products'));
}
}
Kembali ke routes/web.php
kemudian tambahkan route untuk edit data invoice:
Route::group(['prefix' => 'invoice'], function() {
// [.. CODE SEBELUMNYA ..]
Route::get('/{id}', 'InvoiceController@edit')->name('invoice.edit');
});
Kemudian buat view edit.blade.php
untuk meng-handle form memasukkan produk ke invoice, tambahkan code berikut:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<div class="row">
<div class="col-md-12">
<div class="text-center">
<img src="{{ asset('laravel.png') }}" alt="" width="350px" height="150px">
</div>
</div>
<div class="col-md-6">
<table>
<tr>
<td width="30%">Pelanggan</td>
<td>:</td>
<td>{{ $invoice->customer->name }}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{{ $invoice->customer->address }}</td>
</tr>
<tr>
<td>No Telp</td>
<td>:</td>
<td>{{ $invoice->customer->phone }}</td>
</tr>
<tr>
<td>Email</td>
<td>:</td>
<td>{{ $invoice->customer->email }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table>
<tr>
<td width="30%">Perusahaan</td>
<td>:</td>
<td>Daengweb</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>Jl Sultan Hasanuddin Makassar</td>
</tr>
<tr>
<td>No Telp</td>
<td>:</td>
<td>085343966997</td>
</tr>
<tr>
<td>Email</td>
<td>:</td>
<td>[email protected]</td>
</tr>
</table>
</div>
<div class="col-md-12 mt-3">
<form action="{{ route('invoice.update', ['id' => $invoice->id]) }}" method="post">
@csrf
<table class="table table-hover table-bordered">
<thead>
<tr>
<td>#</td>
<td>Produk</td>
<td>Qty</td>
<td>Harga</td>
<td>Subtotal</td>
<th>Action</th>
</tr>
</thead>
<!-- MENAMPILKAN PRODUK YANG TELAH DITAMBAHKAN -->
<tbody>
@php $no = 1 @endphp
@foreach ($invoice->detail as $detail)
<tr>
<td>{{ $no++ }}</td>
<td>{{ $detail->product->title }}</td>
<td>{{ $detail->qty }}</td>
<td>Rp {{ number_format($detail->price) }}</td>
<td>Rp {{ $detail->subtotal }}</td>
<td>
<form action="{{ route('invoice.delete_product', ['id' => $detail->id]) }}" method="post">
@csrf
<input type="hidden" name="_method" value="DELETE" class="form-control">
<button class="btn btn-danger btn-sm">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
<!-- MENAMPILKAN PRODUK YANG TELAH DITAMBAHKAN -->
<!-- FORM UNTUK MEMILIH PRODUK YANG AKAN DITAMBAHKAN -->
<tfoot>
<tr>
<td></td>
<td>
<input type="hidden" name="_method" value="PUT" class="form-control">
<select name="product_id" class="form-control">
<option value="">Pilih Produk</option>
@foreach ($products as $product)
<option value="{{ $product->id }}">{{ $product->title }}</option>
@endforeach
</select>
</td>
<td>
<input type="number" min="1" value="1" name="qty" class="form-control" required>
</td>
<td colspan="3">
<button class="btn btn-primary btn-sm">Tambahkan</button>
</td>
</tr>
</tfoot>
<!-- FORM UNTUK MEMILIH PRODUK YANG AKAN DITAMBAHKAN -->
</table>
</form>
</div>
<!-- MENAMPILKAN TOTAL & TAX -->
<div class="col-md-4 offset-md-8">
<table class="table table-hover table-bordered">
<tr>
<td>Sub Total</td>
<td>:</td>
<td>Rp {{ number_format($invoice->total) }}</td>
</tr>
<tr>
<td>Pajak</td>
<td>:</td>
<td>2% (Rp {{ number_format($invoice->tax) }})</td>
</tr>
<tr>
<td>Total</td>
<td>:</td>
<td>Rp {{ number_format($invoice->total_price) }}</td>
</tr>
</table>
</div>
<!-- MENAMPILKAN TOTAL & TAX -->
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
Penjelasan: Ada beberapa hal yang perlu diperhatikan, pertama $invoice->total_price
, $invoice->tax
, $detail->subtotal
adalah attribute dari accessors yang akan dijelaskan pada sub topik selanjutnya. Selanjutnya, pada form add product akan mengirimkan product_id
dan qty
yang akan ditambahkan kedalam table invoice_details
. Tombol tambahkan akan mengirim data tersebut ke route invoice.update
dan tombol hapus akan mengirimkan data ke route invoice.delete_product
.
Buka file InvoiceController.php
, kemudian tambahkan method berikut:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Customer;
use App\Invoice;
//TAMBAHKAN USE STATEMENT
use App\Product;
use App\Invoice_detail;
class InvoiceController extends Controller
{
// [.. CODE SEBELUMNYA ..]
public function update(Request $request, $id)
{
//VALIDASI
$this->validate($request, [
'product_id' => 'required|exists:products,id',
'qty' => 'required|integer'
]);
try {
//SELECT DARI TABLE invoices BERDASARKAN ID
$invoice = Invoice::find($id);
//SELECT DARI TABLE products BERDASARKAN ID
$product = Product::find($request->product_id);
//SELECT DARI TABLE invoice_details BERDASARKAN product_id & invoice_id
$invoice_detail = $invoice->detail()->where('product_id', $product->id)->first();
//JIKA DATANYA ADA
if ($invoice_detail) {
//MAKA DATA TERSEBUT DI UPDATE QTY NYA
$invoice_detail->update([
'qty' => $invoice_detail->qty + $request->qty
]);
} else {
//JIKA TIDAK MAKA DITAMBAHKAN RECORD BARU
Invoice_detail::create([
'invoice_id' => $invoice->id,
'product_id' => $request->product_id,
'price' => $product->price,
'qty' => $request->qty
]);
}
//KEMUDIAN DI-REDIRECT KEMBALI KE FORM YANG SAMA
return redirect()->back()->with(['success' => 'Product Telah Ditambahkan']);
} catch (\Exception $e) {
return redirect()->back()->with(['error' => $e->getMessage()]);
}
}
public function deleteProduct($id)
{
//SELECT DARI TABLE invoice_details BERDASARKAN ID
$detail = Invoice_detail::find($id);
//KEMUDIAN DIHAPUS
$detail->delete();
//DAN DI-REDIRECT KEMBALI
return redirect()->back()->with(['success' => 'Product telah dihapus']);
}
}
Definisikan route-nya dengan menambahkan code berikut kedalam file routes/web.php
:
Route::group(['prefix' => 'invoice'], function() {
//[.. CODE SEBELUMNYA ..]
Route::put('/{id}', 'InvoiceController@update')->name('invoice.update');
Route::delete('/{id}', 'InvoiceController@deleteProduct')->name('invoice.delete_product');
});
Baca Juga: Fitur Upload Progress Bar Indikator Vue Laravel
Define Accessors & Relationships
Accessor atau yang lebih familiar adalah getters dimana memungkinkan untuk memanipulasi data dari sebuah attribute sebelum ditampilkan atau juga dapat membuat attribute baru (Note: Sebelumnya sudah saya tulis dalam artikel Accessor & Mutator Laravel 5.4). Pada kasus diatas kita membuat attribute baru yakni total_price
, tax
dan sub_total
. Buka file app/Invoice.php
, kemudian tambahkan method berikut:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
protected $guarded = []; //JANGAN LUPA TAMBAHKAN CODE INI
//AGAR DAPAT MENYIMPAN DATA KEDALAM TABLE TERKAIT
//DEFINE ACCESSOR
public function getTaxAttribute()
{
//MENDAPATKAN TAX 2% DARI TOTAL HARGA
return ($this->total * 2) / 100;
}
public function getTotalPriceAttribute()
{
//MENDAPATKAN TOTAL HARGA BARU YANG TELAH DIJUMLAHKAN DENGAN TAX
return ($this->total + (($this->total * 2) / 100));
}
//DEFINE RELATIONSHIPS
public function customer()
{
//Invoice reference ke table customers
return $this->belongsTo(Customer::class);
}
public function detail()
{
//Invoice memiliki hubungan hasMany ke table invoice_detail
return $this->hasMany(Invoice_detail::class);
}
}
Penjelasan: Accessor ditandai dengan format getNamaBaruAttribute()
, dimana NamaBaru adalah nama accessor yang akan dibuat, maka dalam hal ini adalah Tax dan TotalPrice.
Model selanjutnya yakni app/Invoice_detail.php
, tambahkan code berikut:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Invoice_detail extends Model
{
protected $guarded = [];
//DEFINE ACCESSOR
public function getSubtotalAttribute()
{
//NILAI DARI SUBTOTAL ADALAH QTY * PRICE
return number_format($this->qty * $this->price);
}
//DEFINE RELATIONSHIPS
public function product()
{
return $this->belongsTo(Product::class);
}
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
}
Make Observer Event
Event berjalan sesuai dengan trigger yang telah ditetapkan, dalam hal ini kita akan meng-update total
pada table invoices
ketika ada record baru yang telah ditambahkan kedalam table invoice_details
. Tidak hanya ketika data baru tersebut disimpan, juga berlaku ketika terjadi perubahan pada data yang telah ada maupun data tersebut dihapus. Maka akan ada tiga event yang akan berjalan, yakni: created
, updated
dan deleted
.
Buat observer dengan command:
php artisan make:observer Invoice_detailObserver --model=Invoice_detail
Kemudian tambahkan method berikut:
<?php
namespace App\Observers;
//TAMBAHKAN USE STATEMENT
use App\Invoice_detail;
use App\Invoice;
class Invoice_detailObserver
{
//KARENA FUNGSI YANG DIJALANKAN SAMA, MAKA KITA MEMBUATNYA KEDALAM FUNGCTION BARU
private function generateTotal($invoiceDetail)
{
//MENGAMBIL INVOICE_ID
$invoice_id = $invoiceDetail->invoice_id;
//SELECT DARI TABLE invoice_details BERDASARKAN INVOICE
$invoice_detail = Invoice_detail::where('invoice_id', $invoice_id)->get();
//KEMUDIAN DIJUMLAH UNTUK MENDAPATKAN TOTALNYA
$total = $invoice_detail->sum(function($i) {
//DIMANA KETENTUAN YANG DIJUMLAHKAN ADALAH HASIL DARI price* qty
return $i->price * $i->qty;
});
//UPDATE TABLE invoice PADA FIELD TOTAL
$invoiceDetail->invoice()->update([
'total' => $total
]);
}
public function created(Invoice_detail $invoiceDetail)
{
//PANGGIL METHOD BARU TERSEBUT
$this->generateTotal($invoiceDetail);
}
public function updated(Invoice_detail $invoiceDetail)
{
//PANGGIL METHOD BARU TERSEBUT
$this->generateTotal($invoiceDetail);
}
public function deleted(Invoice_detail $invoiceDetail)
{
//PANGGIL METHOD BARU TERSEBUT
$this->generateTotal($invoiceDetail);
}
public function restored(Invoice_detail $invoiceDetail)
{
//
}
/**
* Handle the invoice_detail "force deleted" event.
*
* @param \App\Invoice_detail $invoiceDetail
* @return void
*/
public function forceDeleted(Invoice_detail $invoiceDetail)
{
//
}
}
Agar observer ini dapat berjalan ketika eventnya terpenuhi, maka definisikan terlebih dahulu kedalam Service providers. Buka file app/Providers/AppServiceProvider.php
, kemudian modifikasi menjadi:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
//TAMBAHKAN USE STATEMENT
use App\Invoice_detail;
use App\Observers\Invoice_detailObserver;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//DEFINE OBSERVER YANG TELAH DIBUAT
//Invoce_detail adalah nama class dari model
//Invoice_detailObserver adalah nama class dari observer itu sendiri
Invoice_detail::observe(Invoice_detailObserver::class);
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}
Agar bisa diklik dari menu navigasi, pada file resources/views/layouts/app.blade.php
, tambahkan tag berikut:
<li class="nav-item">
<a href="{{ route('invoice.create') }}" class="nav-link">Buat Invoice</a>
</li>
Maka hasil yang akan diperoleh akan tampak seperti berikut
Create Invoice From Customer
Schemanya adalah ketika user menekan tombol buat invoice dari halaman list pelanggan, maka secara otomatis akan membuat sebuah invoice baru dan diarahkan kehalaman tambah produk kedalam invoice tersebut. Hal yang perlu dimodifikasi adalah file index.blade.php
pada folder resources/views/customer
, lakukan perubahan pada bagian berikut:
<!-- [... CODE SEBELUMNYA ...] -->
<table class="table table-hover table-bordered">
<thead>
<tr>
<th>Nama Lengkap</th>
<th>No Telp</th>
<th>Alamat</th>
<th>Email</th>
<th colspan="2">Aksi</th> <!-- MODIFIKASI INI DENGAN MENAMBAHKAN COLSPAN -->
</tr>
</thead>
<tbody>
@forelse($customers as $customer)
<tr>
<td>{{ $customer->name }}</td>
<td>{{ $customer->phone }}</td>
<td>{{ str_limit($customer->address, 50) }}</td>
<td><a href="mailto:{{ $customer->email }}">{{ $customer->email }}</a></td>
<td>
<form action="{{ url('/customer/' . $customer->id) }}" method="POST">
@csrf
<input type="hidden" name="_method" value="DELETE" class="form-control">
<a href="{{ url('/customer/' . $customer->id) }}" class="btn btn-warning btn-sm">Edit</a>
<button class="btn btn-danger btn-sm">Hapus</button>
</form>
</td>
<!-- [... TAMBAHKAN FORM INI ...] -->
<!-- KARENA YANG DIBUTUHKAN METHOD POST MAKA KITA MEMASUKKANNYA KEDALAM FORM -->
<td>
<form action="{{ route('invoice.store') }}" method="post">
@csrf
<input type="hidden" name="customer_id" value="{{ $customer->id }}" class="form-control">
<button class="btn btn-primary btn-sm">Buat Invoice</button>
</form>
</td>
<!-- [... TAMBAHKAN FORM INI ...] -->
</tr>
@empty
<tr>
<td class="text-center" colspan="5">Tidak ada data</td>
</tr>
@endforelse
</tbody>
</table>
<!-- [... CODE SETELAHNYA ...] -->
Adapun hasil yang akan diperoleh adalah
Kesimpulan
Crud yang dibolak balik untuk menyelesaikan fitur ini, itulah istilah "tepat" yang saya gunakan. Sebab sepanjang artikel ini kita hanya memanipulasi database sesuai dengan kebutuhan. Maka penting adanya untuk memahami cara kerja dari sebuah operasi, sebab kerap kali operasi sederhana yang telah kita pelajari sebelumnya memberikan pengaruh besar dalam proses pembuatan sebuah aplikasi. Sebut saja, CRUD yang terlihat sederhana dan sudah "mainstream" namun banyak digunakan. Benar, bahwa sebuah aplikasi tidak hanya sebatas CRUD namun bukan berarti CRUD tidak memiliki peran didalamnya.
Ohya kamu dapat melihat dokumentasi code dari artikel ini di Github.
Comments