Membuat Dashboard dan Laporan Sistem Kasir Laravel
Diterbitkan 16 September 2026
Pada tahap kesembilan kita akan membuat dashboard dan laporan sistem kasir Laravel.
Tahap ini menggunakan data yang sudah terbentuk dari:
produk
penjualan
pembelian
stok
stock_movements
Tujuannya adalah menampilkan informasi penting secara ringkas agar administrator dapat melihat kondisi toko tanpa membuka seluruh transaksi satu per satu.
Dashboard akan menampilkan:
omzet hari ini
jumlah transaksi hari ini
produk terlaris
stok menipis
stok habis
jumlah produk aktif
Sedangkan laporan akan menyediakan:
filter tanggal
filter status
filter kasir
pencarian invoice
ringkasan omzet
jumlah transaksi
total diskon
daftar transaksi
detail transaksi
Pada struktur tutorial baru, slug lama:
dashboard-laporan-dan-nota
tetap dipertahankan agar URL yang sudah publish tidak berubah.
Namun isi tahap ini difokuskan pada:
dashboard
laporan
Bagian cetak nota akan dibuat lebih detail pada tahap berikutnya dengan slug baru.
Hasil yang Akan Dibuat
Pada tahap ini kita akan membuat:
DashboardController
ReportController
Route:
/dashboard
/reports/sales
View:
resources/views/dashboard/index.blade.php
resources/views/reports/sales.blade.php
Fitur dashboard:
- omzet hari ini;
- transaksi hari ini;
- produk aktif;
- produk stok menipis;
- produk habis;
- produk terlaris;
- transaksi terbaru.
Fitur laporan:
- filter tanggal awal;
- filter tanggal akhir;
- filter kasir;
- filter status;
- pencarian invoice;
- total omzet;
- total transaksi;
- total diskon;
- pagination;
- link detail transaksi.
Persiapan
Pastikan Tahap 01 sampai Tahap 08 sudah selesai.
Aplikasi harus sudah mempunyai data:
users
products
sales
sale_details
purchases
stock_movements
Pastikan sudah ada beberapa transaksi penjualan untuk menguji dashboard dan laporan.
Login sebagai administrator:
admin@example.com
admin12345
1. Prinsip Dashboard
Dashboard bukan tempat menampilkan semua data.
Dashboard sebaiknya menampilkan indikator penting yang dapat dipahami cepat.
Pada tutorial ini kita menggunakan:
hari ini
sebagai periode utama.
Data penjualan hanya menghitung transaksi dengan:
status = paid
Transaksi cancelled tidak ikut dihitung sebagai omzet.
2. Membuat DashboardController
Jalankan:
php artisan make:controller DashboardController
File:
app/Http/Controllers/DashboardController.php
3. Menambahkan Route Dashboard
Buka:
routes/web.php
Tambahkan import:
use App\Http\Controllers\DashboardController;
Tambahkan route:
Route::middleware(['auth'])->group(function () {
Route::get(
'/dashboard',
[DashboardController::class, 'index']
)->name('dashboard');
});
Dashboard dapat dibuka admin maupun kasir.
Namun beberapa informasi sensitif dapat disembunyikan berdasarkan role jika diperlukan.
Untuk tutorial ini keduanya dapat melihat ringkasan operasional.
4. DashboardController Lengkap
Buka:
app/Http/Controllers/DashboardController.php
Isi:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Sale;
use App\Models\SaleDetail;
use Illuminate\View\View;
class DashboardController extends Controller
{
public function index(): View
{
$today = now()->toDateString();
$todaySales = Sale::query()
->where('status', 'paid')
->whereDate('sale_date', $today);
$todayRevenue = (clone $todaySales)
->sum('grand_total');
$todayTransactions = (clone $todaySales)
->count();
$activeProducts = Product::query()
->where('is_active', true)
->count();
$lowStocks = Product::query()
->with(['category', 'unit'])
->where('is_active', true)
->whereColumn('stock', '<=', 'minimum_stock')
->orderBy('stock')
->limit(10)
->get();
$outOfStockCount = Product::query()
->where('is_active', true)
->where('stock', 0)
->count();
$bestSellers = SaleDetail::query()
->selectRaw(
'product_id, SUM(quantity) as total_quantity'
)
->whereHas('sale', function ($query) use ($today) {
$query
->where('status', 'paid')
->whereDate('sale_date', $today);
})
->with('product.unit')
->groupBy('product_id')
->orderByDesc('total_quantity')
->limit(5)
->get();
$latestSales = Sale::query()
->with(['user', 'customer'])
->latest('sale_date')
->limit(10)
->get();
return view('dashboard.index', compact(
'todayRevenue',
'todayTransactions',
'activeProducts',
'lowStocks',
'outOfStockCount',
'bestSellers',
'latestSales'
));
}
}
5. Mengapa Query Sales Di-clone?
Kita membuat query dasar:
$todaySales = Sale::query()
->where('status', 'paid')
->whereDate('sale_date', $today);
Kemudian:
(clone $todaySales)->sum('grand_total');
dan:
(clone $todaySales)->count();
Clone digunakan agar query builder dasar dapat digunakan kembali dengan aman.
6. Omzet Hari Ini
Query:
->sum('grand_total')
hanya menghitung transaksi:
paid
dan:
sale_date = hari ini
Transaksi cancelled tidak masuk.
7. Jumlah Transaksi Hari Ini
Query:
->count()
menghitung jumlah header transaksi paid hari ini.
8. Produk Aktif
Query:
Product::where('is_active', true)->count();
menampilkan jumlah produk yang masih aktif.
9. Stok Menipis
Query:
whereColumn(
'stock',
'<=',
'minimum_stock'
)
mengambil produk yang stoknya sudah berada di bawah atau sama dengan batas minimum.
10. Produk Habis
Query:
where('stock', 0)
menghitung produk aktif yang benar-benar habis.
11. Produk Terlaris
Kita mengambil data dari:
sale_details
bukan dari produk.
Query menjumlahkan:
SUM(quantity)
per:
product_id
dan hanya menggunakan penjualan:
status = paid
hari ini.
12. Transaksi Terbaru
Dashboard menampilkan 10 transaksi terbaru.
Transaksi cancelled tetap dapat tampil dengan statusnya agar admin mengetahui aktivitas terbaru.
13. Membuat View Dashboard
Buat folder:
resources/views/dashboard
Buat:
resources/views/dashboard/index.blade.php
Isi:
@extends('layouts.app')
@section('title', 'Dashboard')
@section('content')
<div class="mb-4">
<h1 class="h4 mb-1">Dashboard</h1>
<p class="text-muted mb-0">
Ringkasan aktivitas sistem kasir hari ini.
</p>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Omzet Hari Ini
</div>
<div class="fs-4 fw-semibold">
Rp {{ number_format(
(float) $todayRevenue,
0,
',',
'.'
) }}
</div>
</div>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Transaksi Hari Ini
</div>
<div class="fs-4 fw-semibold">
{{ $todayTransactions }}
</div>
</div>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Produk Aktif
</div>
<div class="fs-4 fw-semibold">
{{ $activeProducts }}
</div>
</div>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Produk Habis
</div>
<div class="fs-4 fw-semibold">
{{ $outOfStockCount }}
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-xl-7">
<div class="card h-100">
<div class="card-header">
Produk Stok Menipis
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead>
<tr>
<th>Produk</th>
<th>Kategori</th>
<th class="text-center">Stok</th>
<th class="text-center">Minimum</th>
</tr>
</thead>
<tbody>
@forelse($lowStocks as $product)
<tr>
<td>
<div class="fw-semibold">
{{ $product->name }}
</div>
<div class="small text-muted">
{{ $product->sku }}
</div>
</td>
<td>
{{ $product->category?->name ?? '-' }}
</td>
<td class="text-center">
{{ $product->stock }}
{{ $product->unit?->symbol }}
</td>
<td class="text-center">
{{ $product->minimum_stock }}
</td>
</tr>
@empty
<tr>
<td
colspan="4"
class="text-center text-muted">
Tidak ada produk stok menipis.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-5">
<div class="card h-100">
<div class="card-header">
Produk Terlaris Hari Ini
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead>
<tr>
<th>Produk</th>
<th class="text-end">Terjual</th>
</tr>
</thead>
<tbody>
@forelse($bestSellers as $item)
<tr>
<td>
{{ $item->product?->name ?? 'Produk' }}
</td>
<td class="text-end">
{{ $item->total_quantity }}
{{ $item->product?->unit?->symbol }}
</td>
</tr>
@empty
<tr>
<td
colspan="2"
class="text-center text-muted">
Belum ada penjualan hari ini.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Transaksi Terbaru</span>
<a
href="{{ route('sales.index') }}"
class="btn btn-sm btn-outline-primary">
Lihat Semua
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead>
<tr>
<th>Invoice</th>
<th>Tanggal</th>
<th>Kasir</th>
<th>Pelanggan</th>
<th class="text-end">Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@forelse($latestSales as $sale)
<tr>
<td>
<a
href="{{ route('sales.show', $sale) }}">
{{ $sale->invoice_number }}
</a>
</td>
<td>
{{ $sale->sale_date->format('d/m/Y H:i') }}
</td>
<td>
{{ $sale->user?->name ?? '-' }}
</td>
<td>
{{ $sale->customer?->name ?? 'Umum' }}
</td>
<td class="text-end">
Rp {{ number_format(
(float) $sale->grand_total,
0,
',',
'.'
) }}
</td>
<td>
@if($sale->status === 'paid')
<span class="badge text-bg-success">
Dibayar
</span>
@else
<span class="badge text-bg-danger">
Dibatalkan
</span>
@endif
</td>
</tr>
@empty
<tr>
<td
colspan="6"
class="text-center text-muted">
Belum ada transaksi.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endsection
14. Membuat ReportController
Sekarang kita membuat laporan penjualan.
Jalankan:
php artisan make:controller ReportController
File:
app/Http/Controllers/ReportController.php
15. Menambahkan Route Laporan
Tambahkan import:
use App\Http\Controllers\ReportController;
Tambahkan route admin:
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get(
'reports/sales',
[ReportController::class, 'sales']
)->name('reports.sales');
});
Laporan hanya dapat diakses administrator.
16. Menambahkan Menu Laporan
Pada layout:
@if(auth()->user()->role === 'admin')
<li class="nav-item">
<a
class="nav-link"
href="{{ route('reports.sales') }}">
Laporan Penjualan
</a>
</li>
@endif
17. Import ReportController
Gunakan:
use App\Models\Sale;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\View\View;
18. ReportController Lengkap
Buka:
app/Http/Controllers/ReportController.php
Isi:
<?php
namespace App\Http\Controllers;
use App\Models\Sale;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ReportController extends Controller
{
public function sales(Request $request): View
{
$validated = $request->validate([
'start_date' => [
'nullable',
'date',
],
'end_date' => [
'nullable',
'date',
'after_or_equal:start_date',
],
'status' => [
'nullable',
'in:paid,cancelled',
],
'user_id' => [
'nullable',
'exists:users,id',
],
'q' => [
'nullable',
'string',
'max:100',
],
]);
$startDate = $validated['start_date']
?? now()->startOfMonth()->toDateString();
$endDate = $validated['end_date']
?? now()->toDateString();
$status = $validated['status'] ?? 'paid';
$userId = $validated['user_id'] ?? null;
$search = trim((string) ($validated['q'] ?? ''));
$baseQuery = Sale::query()
->whereDate('sale_date', '>=', $startDate)
->whereDate('sale_date', '<=', $endDate)
->when(
$status !== '',
fn ($query) => $query->where('status', $status)
)
->when(
$userId,
fn ($query) => $query->where('user_id', $userId)
)
->when(
$search !== '',
fn ($query) => $query->where(
'invoice_number',
'like',
'%' . $search . '%'
)
);
$totalRevenue = (clone $baseQuery)
->where('status', 'paid')
->sum('grand_total');
$totalDiscount = (clone $baseQuery)
->where('status', 'paid')
->sum('discount');
$totalTransactions = (clone $baseQuery)
->count();
$sales = (clone $baseQuery)
->with(['user', 'customer'])
->latest('sale_date')
->paginate(20)
->withQueryString();
$users = User::query()
->whereIn('role', ['admin', 'cashier'])
->orderBy('name')
->get([
'id',
'name',
'role',
]);
return view('reports.sales', compact(
'sales',
'users',
'startDate',
'endDate',
'status',
'userId',
'search',
'totalRevenue',
'totalDiscount',
'totalTransactions'
));
}
}
19. Default Periode Laporan
Jika user tidak memilih tanggal:
start_date = awal bulan
end_date = hari ini
Kode:
now()->startOfMonth()->toDateString()
dan:
now()->toDateString()
20. Default Status Laporan
Default:
paid
Artinya laporan awal menampilkan transaksi yang benar-benar menjadi penjualan.
Transaksi cancelled dapat dilihat dengan filter status.
21. Total Revenue
Ringkasan omzet hanya menghitung:
status = paid
Walaupun user memilih filter tertentu, omzet tidak boleh menjumlahkan cancelled sebagai pendapatan.
22. Total Discount
Diskon juga hanya dihitung dari transaksi paid.
23. Total Transactions
totalTransactions mengikuti filter base query.
Jika status:
cancelled
maka total transaksi menunjukkan jumlah cancelled sesuai filter.
24. Membuat View Laporan
Buat folder:
resources/views/reports
Buat:
resources/views/reports/sales.blade.php
Isi:
@extends('layouts.app')
@section('title', 'Laporan Penjualan')
@section('content')
<div class="mb-3">
<h1 class="h4 mb-1">
Laporan Penjualan
</h1>
<p class="text-muted mb-0">
Filter transaksi berdasarkan periode, status, dan kasir.
</p>
</div>
<div class="card mb-3">
<div class="card-body">
<form
method="GET"
action="{{ route('reports.sales') }}"
class="row g-2">
<div class="col-lg-2">
<label class="form-label">
Tanggal Awal
</label>
<input
type="date"
name="start_date"
value="{{ $startDate }}"
class="form-control">
</div>
<div class="col-lg-2">
<label class="form-label">
Tanggal Akhir
</label>
<input
type="date"
name="end_date"
value="{{ $endDate }}"
class="form-control">
</div>
<div class="col-lg-2">
<label class="form-label">
Status
</label>
<select
name="status"
class="form-select">
<option
value="paid"
@selected($status === 'paid')}>
Dibayar
</option>
<option
value="cancelled"
@selected($status === 'cancelled')}>
Dibatalkan
</option>
<option
value=""
@selected($status === '')>
Semua
</option>
</select>
</div>
<div class="col-lg-2">
<label class="form-label">
Kasir
</label>
<select
name="user_id"
class="form-select">
<option value="">
Semua Kasir
</option>
@foreach($users as $user)
<option
value="{{ $user->id }}"
@selected(
(string) $userId
===
(string) $user->id
)>
{{ $user->name }}
</option>
@endforeach
</select>
</div>
<div class="col-lg-2">
<label class="form-label">
Invoice
</label>
<input
type="text"
name="q"
value="{{ $search }}"
class="form-control"
placeholder="INV-...">
</div>
<div class="col-lg-2 d-flex align-items-end">
<div class="d-flex gap-2 w-100">
<button
class="btn btn-primary">
Filter
</button>
<a
href="{{ route('reports.sales') }}"
class="btn btn-secondary">
Reset
</a>
</div>
</div>
</form>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Omzet
</div>
<div class="fs-4 fw-semibold">
Rp {{ number_format(
(float) $totalRevenue,
0,
',',
'.'
) }}
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Total Diskon
</div>
<div class="fs-4 fw-semibold">
Rp {{ number_format(
(float) $totalDiscount,
0,
',',
'.'
) }}
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small">
Jumlah Transaksi
</div>
<div class="fs-4 fw-semibold">
{{ $totalTransactions }}
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th>No</th>
<th>Invoice</th>
<th>Tanggal</th>
<th>Kasir</th>
<th>Pelanggan</th>
<th class="text-end">Subtotal</th>
<th class="text-end">Diskon</th>
<th class="text-end">Total</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@forelse($sales as $sale)
<tr>
<td>
{{ $sales->firstItem() + $loop->index }}
</td>
<td>
<code>
{{ $sale->invoice_number }}
</code>
</td>
<td>
{{ $sale->sale_date->format('d/m/Y H:i') }}
</td>
<td>
{{ $sale->user?->name ?? '-' }}
</td>
<td>
{{ $sale->customer?->name ?? 'Umum' }}
</td>
<td class="text-end">
Rp {{ number_format(
(float) $sale->subtotal,
0,
',',
'.'
) }}
</td>
<td class="text-end">
Rp {{ number_format(
(float) $sale->discount,
0,
',',
'.'
) }}
</td>
<td class="text-end">
Rp {{ number_format(
(float) $sale->grand_total,
0,
',',
'.'
) }}
</td>
<td>
@if($sale->status === 'paid')
<span class="badge text-bg-success">
Dibayar
</span>
@else
<span class="badge text-bg-danger">
Dibatalkan
</span>
@endif
</td>
<td>
<a
href="{{ route('sales.show', $sale) }}"
class="btn btn-sm btn-outline-primary">
Detail
</a>
</td>
</tr>
@empty
<tr>
<td
colspan="10"
class="text-center text-muted">
Tidak ada transaksi sesuai filter.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $sales->links() }}
</div>
</div>
@endsection
25. Validasi Periode
Laporan menggunakan:
'after_or_equal:start_date'
untuk:
end_date
Artinya tanggal akhir tidak boleh lebih kecil dari tanggal awal.
26. Filter Kasir
Filter menggunakan:
user_id
dan memastikan ID user valid melalui:
exists:users,id
Daftar user dibatasi:
admin
cashier
27. Filter Invoice
Pencarian invoice menggunakan:
LIKE
Contoh:
INV-20260916
dapat menampilkan seluruh transaksi pada pola invoice tersebut.
28. Mengapa Tidak Menghitung Cancelled sebagai Revenue?
Transaksi cancelled hanya histori.
Pendapatan hanya berasal dari:
paid
Karena itu dashboard dan ringkasan omzet harus selalu memisahkan cancelled.
29. Menguji Dashboard
Buat beberapa transaksi hari ini.
Contoh:
Transaksi A = 20.000
Transaksi B = 30.000
Dashboard harus menampilkan:
Omzet = 50.000
Transaksi = 2
Jika transaksi B dibatalkan:
Omzet = 20.000
Transaksi = 1
untuk indikator paid hari ini.
30. Menguji Produk Terlaris
Contoh:
Air Mineral terjual 5
Kopi Sachet terjual 2
Dashboard harus mengurutkan:
Air Mineral
Kopi Sachet
berdasarkan jumlah terjual.
31. Menguji Stok Menipis
Ubah stok melalui transaksi sampai:
stock <= minimum_stock
Produk harus muncul di dashboard.
32. Menguji Laporan Bulan Berjalan
Buka:
/reports/sales
Tanpa filter, periode default:
awal bulan sampai hari ini
status default:
paid
33. Menguji Filter Tanggal
Pilih:
start_date = 2026-09-01
end_date = 2026-09-15
Hanya transaksi pada periode tersebut yang tampil.
34. Menguji Filter Cancelled
Pilih:
status = cancelled
Daftar hanya menampilkan transaksi dibatalkan.
Ringkasan omzet tetap:
0
karena cancelled bukan pendapatan.
35. Menguji Semua Status
Pilih:
status = semua
Daftar menampilkan:
paid
cancelled
Namun omzet hanya menjumlahkan paid.
36. Menguji Filter Kasir
Pilih salah satu kasir.
Daftar hanya menampilkan transaksi user tersebut.
37. Menguji Pagination
Jika transaksi lebih dari 20, pagination muncul.
Karena menggunakan:
withQueryString()
semua filter tetap dipertahankan ketika berpindah halaman.
38. Menguji Tanggal Salah
Masukkan:
start_date = 2026-09-20
end_date = 2026-09-10
Validasi harus menolak.
39. Menguji Akses Kasir ke Laporan
Login sebagai kasir.
Buka:
/reports/sales
Akses harus ditolak karena route laporan menggunakan:
role:admin
40. Dashboard Tetap Bisa Dibuka Kasir
Kasir dapat membuka:
/dashboard
karena dashboard hanya menggunakan:
auth
Jika ingin menyembunyikan omzet dari kasir, view dapat diberi kondisi role.
Contoh:
@if(auth()->user()->role === 'admin')
...
@endif
Namun tutorial dasar tetap menampilkan dashboard yang sama.
41. Struktur File Setelah Tahap 09
Controller:
app/Http/Controllers/
├── DashboardController.php
└── ReportController.php
View:
resources/views/dashboard/
└── index.blade.php
resources/views/reports/
└── sales.blade.php
Route:
dashboard
reports.sales
42. Checklist Dashboard
Pastikan:
[ ] omzet hari ini tampil
[ ] transaksi hari ini tampil
[ ] produk aktif tampil
[ ] produk habis tampil
[ ] stok menipis tampil
[ ] produk terlaris tampil
[ ] transaksi terbaru tampil
[ ] cancelled tidak dihitung sebagai omzet
43. Checklist Laporan
Pastikan:
[ ] periode default benar
[ ] filter tanggal bekerja
[ ] filter status bekerja
[ ] filter kasir bekerja
[ ] pencarian invoice bekerja
[ ] omzet benar
[ ] diskon benar
[ ] jumlah transaksi benar
[ ] pagination bekerja
[ ] filter tetap saat pagination
[ ] link detail transaksi bekerja
44. Kesalahan Umum: Dashboard Menghitung Cancelled
Pastikan query memakai:
where('status', 'paid')
untuk omzet dan jumlah transaksi paid.
45. Kesalahan Umum: Produk Terlaris Menghitung Cancelled
Pastikan:
whereHas('sale', function ($query) {
$query->where('status', 'paid');
})
46. Kesalahan Umum: Laporan Menggunakan created_at
Kita menggunakan:
sale_date
karena itu adalah tanggal transaksi bisnis.
created_at adalah waktu record dibuat.
Keduanya dapat berbeda.
47. Kesalahan Umum: Omzet Mengikuti Status Semua
Jika filter:
status = semua
jangan menjumlahkan cancelled.
Gunakan:
where('status', 'paid')
pada query ringkasan omzet.
48. Kesalahan Umum: Query N+1
Gunakan:
with(['user', 'customer'])
ketika menampilkan daftar transaksi.
49. Kesalahan Umum: Produk Terlaris Tidak Group By
Pastikan:
groupBy('product_id')
agar quantity dijumlahkan per produk.
50. Pengembangan Lanjutan
Dashboard dan laporan dapat dikembangkan dengan:
grafik omzet harian
grafik omzet bulanan
laba kotor
produk paling menguntungkan
laporan pembelian
laporan stok
export Excel
export PDF
perbandingan periode
dashboard per kasir
Untuk tutorial dasar, fitur saat ini sudah cukup untuk memberikan ringkasan operasional.
51. Tentang Laba
Tutorial ini belum menghitung laba secara akuntansi.
Walaupun produk mempunyai:
purchase_price
selling_price
menghitung laba historis yang benar memerlukan keputusan metode biaya.
Contoh:
harga beli berubah
stok berasal dari beberapa batch
Karena itu dashboard saat ini fokus pada:
omzet
jumlah transaksi
produk terlaris
stok
bukan laba bersih.
52. Mengapa Nota Dipisahkan?
Pada versi lama, artikel ini mencakup:
dashboard
laporan
nota
Namun jika nota dibahas lengkap, dibutuhkan:
data toko
layout thermal
58 mm
80 mm
print CSS
route cetak
setting toko
auto print opsional
Agar tutorial tidak terlalu padat, nota dipisahkan menjadi tahap berikutnya.
URL lama:
dashboard-laporan-dan-nota
tetap dipertahankan.
53. Checkpoint Hasil
Setelah Tahap 09:
Login : selesai
Role admin/kasir : selesai
Database : selesai
Model dan relasi : selesai
Seeder : selesai
CRUD master : selesai
CRUD produk : selesai
Penjualan : selesai
Pembelian : selesai
Kartu stok : selesai
Penyesuaian stok : selesai
Dashboard : selesai
Laporan penjualan : selesai
Produk terlaris : selesai
Stok menipis : selesai
Nota : tahap berikutnya
Testing : belum
Deploy : belum
54. Ringkasan
Pada tahap kesembilan kita telah membuat dashboard dan laporan sistem kasir.
Dashboard menampilkan:
omzet hari ini
jumlah transaksi
produk aktif
produk habis
stok menipis
produk terlaris
transaksi terbaru
Laporan penjualan menyediakan:
filter tanggal
filter status
filter kasir
pencarian invoice
omzet
diskon
jumlah transaksi
pagination
detail transaksi
Seluruh ringkasan pendapatan hanya menghitung:
status = paid
Transaksi cancelled tetap tersedia untuk histori tetapi tidak dihitung sebagai omzet.
Tahap berikutnya kita akan membuat cetak nota kasir dengan tampilan thermal 58 mm dan 80 mm.





Komentar
Tulis Komentar