Merge pull request #5 from LailaWulandarii/development

Development
This commit is contained in:
Laila Wulandari 2026-01-27 17:24:29 +07:00 committed by GitHub
commit b89c909a95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 895 additions and 770 deletions

View File

@ -3,68 +3,24 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Additional; use App\Models\Additional;
use Illuminate\Support\Facades\Validator; use App\Http\Requests\Admin\AdditionalRequest; // Gunakan FormRequest baru
class AdditionalController extends Controller class AdditionalController extends Controller
{ {
// Store: Simpan Data Baru public function store(AdditionalRequest $request)
public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ Additional::create($request->validated());
'nama' => 'required|string|min:3|max:100',
'harga' => 'required|numeric|min:0',
], [
'required' => 'Kolom :attribute wajib diisi.',
'numeric' => ':attribute harus berupa angka.',
'string' => 'Input :attribute harus berupa teks valid.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'numeric' => ':attribute harus berupa angka.',
], [
'nama' => 'nama additional',
'harga' => 'harga additional',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_modal', 'createAdd');
}
Additional::create($request->only(['nama', 'harga']));
return redirect()->back()->with('success', 'Additional berhasil ditambahkan!'); return redirect()->back()->with('success', 'Additional berhasil ditambahkan!');
} }
// Update: Simpan Perubahan public function update(AdditionalRequest $request, string $id)
public function update(Request $request, string $id)
{ {
$additional = Additional::findOrFail($id); $additional = Additional::findOrFail($id);
$validator = Validator::make($request->all(), [ $additional->update($request->validated());
'nama' => 'required|string|min:3|max:100',
'harga' => 'required|numeric|min:0',
], [
'required' => 'Kolom :attribute wajib diisi.',
'numeric' => ':attribute harus berupa angka.',
'string' => 'Input :attribute harus berupa teks valid.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'numeric' => ':attribute harus berupa angka.',
], [
'nama' => 'nama additional',
'harga' => 'harga additional',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_id_add', $id);
}
$additional->update($request->only(['nama', 'harga']));
return redirect()->back()->with('success', 'Additional berhasil diperbarui!'); return redirect()->back()->with('success', 'Additional berhasil diperbarui!');
} }
// Destroy: Hapus Data
public function destroy(string $id) public function destroy(string $id)
{ {
$additional = Additional::findOrFail($id); $additional = Additional::findOrFail($id);

View File

@ -7,55 +7,66 @@
use App\Models\BookingFoto; use App\Models\BookingFoto;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Services\DashboardService;
class BerandaController extends Controller class BerandaController extends Controller
{ {
public function index() public function index()
{ {
$now = Carbon::now();
$lastMonth = Carbon::now()->subMonth();
$today = Carbon::today(); $today = Carbon::today();
$getGrowth = function ($current, $previous) { $stat = DashboardService::getStats();
if ($previous <= 0) return $current > 0 ? 100 : 0; $buketToday = TransaksiBuket::with(['pelanggan', 'buket'])->whereDate('tgl_ambil', $today)->where('status_transaksi', 'diterima')->get();
return round((($current - $previous) / $previous) * 100, 1); $fotoToday = BookingFoto::with(['pelanggan', 'paketFoto'])->whereDate('tgl_booking', $today)->where('status_booking', 'diterima')->orderBy('jam_mulai', 'asc')->get();
}; $pesananBuket = TransaksiBuket::where('status_transaksi', 'menunggu_verifikasi')->latest()->get();
$currPendapatan = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'diterima')->sum('total_bayar') + $pesananFoto = BookingFoto::where('status_booking', 'menunggu_verifikasi')->latest()->get();
BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'diterima')->sum('total_bayar');
$currMasuk = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->count() +
BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->count();
$currSelesai = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'selesai')->count() +
BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'selesai')->count();
$currBatal = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'ditolak')->count() +
BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'ditolak')->count();
$prevPendapatan = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'diterima')->sum('total_bayar') +
BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'diterima')->sum('total_bayar');
$prevMasuk = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->count() +
BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->count();
$prevSelesai = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'selesai')->count() +
BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'selesai')->count();
$prevBatal = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'ditolak')->count() +
BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'ditolak')->count();
$stat = [
'pendapatan' => $currPendapatan,
'pendapatan_grow' => $getGrowth($currPendapatan, $prevPendapatan),
'masuk_count' => $currMasuk,
'masuk_grow' => $getGrowth($currMasuk, $prevMasuk),
'selesai_count' => $currSelesai,
'selesai_grow' => $getGrowth($currSelesai, $prevSelesai),
'batal_count' => $currBatal,
'batal_grow' => $getGrowth($currBatal, $prevBatal),
];
$buketToday = TransaksiBuket::with(['pelanggan', 'buket'])->whereDate('tgl_ambil', $today)->where('status_transaksi', 'diterima')->get();
$fotoToday = BookingFoto::with(['pelanggan', 'paketFoto'])->whereDate('tgl_booking', $today)->where('status_booking', 'diterima')->orderBy('jam_mulai', 'asc')->get();
$pesananBuket = TransaksiBuket::with(['pelanggan', 'buket'])->where('status_transaksi', 'menunggu_verifikasi')->latest()->get();
$pesananFoto = BookingFoto::with(['pelanggan', 'paketFoto'])->where('status_booking', 'menunggu_verifikasi')->latest()->get();
return view('admin.beranda.index', compact('stat', 'buketToday', 'fotoToday', 'pesananBuket', 'pesananFoto')); return view('admin.beranda.index', compact('stat', 'buketToday', 'fotoToday', 'pesananBuket', 'pesananFoto'));
} }
// public function index()
// {
// $now = Carbon::now();
// $lastMonth = Carbon::now()->subMonth();
// $today = Carbon::today();
// $getGrowth = function ($current, $previous) {
// if ($previous <= 0) return $current > 0 ? 100 : 0;
// return round((($current - $previous) / $previous) * 100, 1);
// };
// $currPendapatan = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'diterima')->sum('total_bayar') +
// BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'diterima')->sum('total_bayar');
// $currMasuk = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->count() +
// BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->count();
// $currSelesai = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'selesai')->count() +
// BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'selesai')->count();
// $currBatal = TransaksiBuket::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_transaksi', 'ditolak')->count() +
// BookingFoto::whereMonth('created_at', $now->month)->whereYear('created_at', $now->year)->where('status_booking', 'ditolak')->count();
// $prevPendapatan = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'diterima')->sum('total_bayar') +
// BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'diterima')->sum('total_bayar');
// $prevMasuk = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->count() +
// BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->count();
// $prevSelesai = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'selesai')->count() +
// BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'selesai')->count();
// $prevBatal = TransaksiBuket::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_transaksi', 'ditolak')->count() +
// BookingFoto::whereMonth('created_at', $lastMonth->month)->whereYear('created_at', $lastMonth->year)->where('status_booking', 'ditolak')->count();
// $stat = [
// 'pendapatan' => $currPendapatan,
// 'pendapatan_grow' => $getGrowth($currPendapatan, $prevPendapatan),
// 'masuk_count' => $currMasuk,
// 'masuk_grow' => $getGrowth($currMasuk, $prevMasuk),
// 'selesai_count' => $currSelesai,
// 'selesai_grow' => $getGrowth($currSelesai, $prevSelesai),
// 'batal_count' => $currBatal,
// 'batal_grow' => $getGrowth($currBatal, $prevBatal),
// ];
// $buketToday = TransaksiBuket::with(['pelanggan', 'buket'])->whereDate('tgl_ambil', $today)->where('status_transaksi', 'diterima')->get();
// $fotoToday = BookingFoto::with(['pelanggan', 'paketFoto'])->whereDate('tgl_booking', $today)->where('status_booking', 'diterima')->orderBy('jam_mulai', 'asc')->get();
// $pesananBuket = TransaksiBuket::with(['pelanggan', 'buket'])->where('status_transaksi', 'menunggu_verifikasi')->latest()->get();
// $pesananFoto = BookingFoto::with(['pelanggan', 'paketFoto'])->where('status_booking', 'menunggu_verifikasi')->latest()->get();
// return view('admin.beranda.index', compact('stat', 'buketToday', 'fotoToday', 'pesananBuket', 'pesananFoto'));
// }
} }

View File

@ -4,8 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Buket; use App\Models\Buket;
use Illuminate\Http\Request; use App\Http\Requests\Admin\BuketRequest; // Panggil Request baru kamu
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class BuketController extends Controller class BuketController extends Controller
@ -16,104 +15,29 @@ public function index()
return view('admin.produk-buket.index', compact('buket')); return view('admin.produk-buket.index', compact('buket'));
} }
public function store(Request $request) public function store(BuketRequest $request)
{ {
$validator = Validator::make($request->all(), [ $data = $request->validated();
'nama' => 'required|string|min:3|max:100',
'ukuran' => 'required|in:S,M,L',
'kategori' => 'required|in:single,fresh,premium_fresh,artificial',
'harga' => 'required|numeric|min:0',
'request_khusus' => 'nullable|string|max:255',
'deskripsi' => 'required|string|min:10',
'foto' => 'required|image|mimes:jpeg,png,jpg|max:2048',
], [
'required' => 'Kolom :attribute tidak boleh kosong.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal harus :min karakter.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'in' => 'Pilihan :attribute tidak sesuai dengan data yang tersedia.',
'numeric' => ':attribute harus berupa angka.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung. Gunakan format: jpeg, png, atau jpg.',
'foto.max' => 'Ukuran :attribute terlalu besar, maksimal adalah 2MB.',
], [
'nama' => 'nama buket',
'ukuran' => 'ukuran buket',
'kategori' => 'kategori buket',
'harga' => 'harga',
'request_khusus' => 'request khusus',
'deskripsi' => 'deskripsi produk',
'foto' => 'foto produk',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_modal', 'create');
}
$path = null;
if ($request->hasFile('foto')) { if ($request->hasFile('foto')) {
$file = $request->file('foto'); $file = $request->file('foto');
$filename = time() . '_' . $file->getClientOriginalName(); $filename = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs('img/buket', $filename, 'public'); $data['foto'] = $file->storeAs('img/buket', $filename, 'public');
} }
Buket::create([ Buket::create($data);
'nama' => $request->nama,
'ukuran' => $request->ukuran,
'kategori' => $request->kategori,
'harga' => $request->harga,
'request_khusus' => $request->request_khusus,
'deskripsi' => $request->deskripsi,
'foto' => $path,
]);
return redirect()->back()->with('success', 'Produk buket berhasil ditambahkan!'); return redirect()->back()->with('success', 'Produk buket berhasil ditambahkan!');
} }
public function update(Request $request, string $id) public function update(BuketRequest $request, string $id)
{ {
$buket = Buket::findOrFail($id); $buket = Buket::findOrFail($id);
$validator = Validator::make($request->all(), [ $data = $request->validated();
'nama' => 'required|string|min:3|max:100',
'ukuran' => 'required|in:S,M,L',
'kategori' => 'required|in:single,fresh,premium_fresh,artificial',
'harga' => 'required|numeric|min:0',
'request_khusus' => 'nullable|string|max:255',
'deskripsi' => 'required|string|min:10',
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
], [
'required' => 'Kolom :attribute tidak boleh kosong.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal harus :min karakter.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'in' => 'Pilihan :attribute tidak sesuai dengan data yang tersedia.',
'numeric' => ':attribute harus berupa angka.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung. Gunakan format: jpeg, png, atau jpg.',
'foto.max' => 'Ukuran :attribute terlalu besar, maksimal adalah 2MB.',
], [
'nama' => 'nama buket',
'ukuran' => 'ukuran buket',
'kategori' => 'kategori buket',
'harga' => 'harga',
'request_khusus' => 'request khusus',
'deskripsi' => 'deskripsi produk',
'foto' => 'foto produk',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_id', $id);
}
$data = $request->only(['nama', 'ukuran', 'kategori', 'harga', 'request_khusus', 'deskripsi']);
if ($request->hasFile('foto')) { if ($request->hasFile('foto')) {
if ($buket->foto) { if ($buket->foto) {
Storage::disk('public')->delete($buket->foto); Storage::disk('public')->delete($buket->foto);
} }
$file = $request->file('foto'); $file = $request->file('foto');
$filename = time() . '_' . $file->getClientOriginalName(); $filename = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs('img/buket', $filename, 'public'); $data['foto'] = $file->storeAs('img/buket', $filename, 'public');
$data['foto'] = $path;
} }
$buket->update($data); $buket->update($data);
return redirect()->back()->with('success', 'Produk buket berhasil diperbarui!'); return redirect()->back()->with('success', 'Produk buket berhasil diperbarui!');
@ -122,12 +46,10 @@ public function update(Request $request, string $id)
public function destroy(string $id) public function destroy(string $id)
{ {
$buket = Buket::findOrFail($id); $buket = Buket::findOrFail($id);
if ($buket->foto) { if ($buket->foto) {
Storage::disk('public')->delete($buket->foto); Storage::disk('public')->delete($buket->foto);
} }
$buket->delete(); $buket->delete();
return redirect()->back()->with('success', 'Produk dan foto berhasil dihapus permanen!'); return redirect()->back()->with('success', 'Produk dan foto berhasil dihapus permanen!');
} }
} }

View File

@ -5,15 +5,11 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Additional; use App\Models\Additional;
use App\Models\PaketFoto; use App\Models\PaketFoto;
use Illuminate\Http\Request; use App\Http\Requests\Admin\FotoRequest; // Gunakan Request baru
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class FotoController extends Controller class FotoController extends Controller
{ {
/**
* Display a listing of the resource.
*/
public function index() public function index()
{ {
$foto = PaketFoto::latest()->get(); $foto = PaketFoto::latest()->get();
@ -21,100 +17,30 @@ public function index()
return view('admin.paket-foto.index', compact('foto', 'additional')); return view('admin.paket-foto.index', compact('foto', 'additional'));
} }
public function store(Request $request) public function store(FotoRequest $request)
{ {
$validator = Validator::make($request->all(), [ $data = $request->validated();
'nama' => 'required|string|min:3|max:100', if ($request->hasFile('foto')) {
'harga' => 'required|numeric|min:0', $file = $request->file('foto');
'durasi' => 'required|integer|min:0', $filename = time() . '_' . $file->getClientOriginalName();
'deskripsi' => 'required|string|min:10', $path = $file->storeAs('img/foto', $filename, 'public');
'foto' => 'required|image|mimes:jpeg,png,jpg|max:2048', $data['foto'] = $path;
], [
'required' => 'Kolom :attribute wajib diisi.',
'string' => 'Input :attribute harus berupa teks valid.',
'integer' => 'Input :attribute harus berupa angka valid.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung. Gunakan format: jpeg, png, atau jpg.',
'foto.max' => 'Ukuran :attribute maksimal 2MB.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'numeric' => ':attribute harus berupa angka.',
], [
'nama' => 'nama paket',
'harga' => 'harga paket',
'durasi' => 'durasi paket',
'foto' => 'foto paket',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_modal', 'createFoto');
} }
if (!$request->hasFile('foto')) { PaketFoto::create($data);
return redirect()->back()
->with('error', 'File foto tidak ditemukan!')
->withInput();
}
$file = $request->file('foto');
$filename = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs('img/foto', $filename, 'public');
if (!$path) {
return redirect()->back()
->with('error', 'Gagal upload file!')
->withInput();
}
PaketFoto::create([
'nama' => $request->nama,
'harga' => $request->harga,
'durasi' => $request->durasi,
'deskripsi' => $request->deskripsi,
'foto' => $path,
]);
return redirect()->back()->with('success', 'Paket foto baru berhasil ditambahkan!'); return redirect()->back()->with('success', 'Paket foto baru berhasil ditambahkan!');
} }
public function update(Request $request, string $id) public function update(FotoRequest $request, string $id)
{ {
$paket = PaketFoto::findOrFail($id); $paket = PaketFoto::findOrFail($id);
$validator = Validator::make($request->all(), [ $data = $request->validated();
'nama' => 'required|string|min:3|max:100',
'harga' => 'required|numeric|min:0',
'durasi' => 'required|integer|min:0',
'deskripsi' => 'required|string|min:10',
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
], [
'required' => 'Kolom :attribute wajib diisi.',
'string' => 'Input :attribute harus berupa teks valid.',
'integer' => 'Input :attribute harus berupa angka valid.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung. Gunakan format: jpeg, png, atau jpg.',
'foto.max' => 'Ukuran :attribute maksimal 2MB.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'numeric' => ':attribute harus berupa angka.',
], [
'nama' => 'nama paket',
'harga' => 'harga paket',
'durasi' => 'durasi paket',
'foto' => 'foto paket',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_id_foto', $id);
}
$data = $request->only(['nama', 'harga', 'durasi', 'deskripsi']);
if ($request->hasFile('foto')) { if ($request->hasFile('foto')) {
if ($paket->foto) { if ($paket->foto) {
Storage::disk('public')->delete($paket->foto); Storage::disk('public')->delete($paket->foto);
} }
$file = $request->file('foto'); $file = $request->file('foto');
$filename = time() . '_' . $file->getClientOriginalName(); $filename = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs('img/foto', $filename, 'public'); $data['foto'] = $file->storeAs('img/foto', $filename, 'public');
$data['foto'] = $path;
} }
$paket->update($data); $paket->update($data);
return redirect()->back()->with('success', 'Paket foto berhasil diperbarui!'); return redirect()->back()->with('success', 'Paket foto berhasil diperbarui!');
@ -122,14 +48,11 @@ public function update(Request $request, string $id)
public function destroy(string $id) public function destroy(string $id)
{ {
// Cari data berdasarkan primary key id_paket $paket = PaketFoto::findOrFail($id);
$paket = PaketFoto::where('id_paket', $id)->firstOrFail();
if ($paket->foto) { if ($paket->foto) {
Storage::disk('public')->delete($paket->foto); Storage::disk('public')->delete($paket->foto);
} }
$paket->delete(); $paket->delete();
return redirect()->back()->with('success', 'Paket foto dan filenya berhasil dihapus!'); return redirect()->back()->with('success', 'Paket foto dan filenya berhasil dihapus!');
} }
} }

View File

@ -4,14 +4,11 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use App\Http\Requests\Admin\AdminRequest; // Panggil Request Baru
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\{Auth, Hash};
use Illuminate\Support\Facades\Hash; use Illuminate\Routing\Controllers\{HasMiddleware, Middleware};
use Illuminate\Support\Facades\Validator; // <--- WAJIB ADA INI
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class ManajemenAdminController extends Controller class ManajemenAdminController extends Controller implements HasMiddleware
{ {
public static function middleware(): array public static function middleware(): array
{ {
@ -24,112 +21,38 @@ public static function middleware(): array
}), }),
]; ];
} }
public function index() public function index()
{ {
$admin = User::where('role', '!=', 'pemilik') $admin = User::where('role', '!=', 'pemilik')->latest()->get();
->latest()
->get();
return view('admin.kelola-admin.index', compact('admin')); return view('admin.kelola-admin.index', compact('admin'));
} }
public function store(Request $request) public function store(AdminRequest $request)
{ {
$validator = Validator::make($request->all(), [ $data = $request->validated();
'nama' => 'required|string|min:5|max:100', $data['password'] = Hash::make($request->username);
'username' => 'required|string|alpha_dash|max:50|unique:users,username', User::create($data);
'email' => 'required|email:dns|max:255|unique:users,email,',
'no_wa' => 'required|numeric|digits_between:10,15',
'role' => 'required|in:admin_foto,admin_buket',
'alamat' => 'required|string|max:255',
], [
'required' => 'Kolom :attribute wajib diisi.',
'unique' => ':attribute sudah terdaftar di sistem, gunakan yang lain.',
'min' => ':attribute minimal harus berisi :min karakter.',
'max' => ':attribute maksimal hanya boleh :max karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus berjumlah antara :min sampai :max digit.',
'email' => 'Format :attribute tidak valid.',
'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, serta simbol - dan _',
'in' => ':attribute yang dipilih tidak sesuai dengan pilihan yang tersedia.',
], [
'nama' => 'nama lengkap',
'username' => 'username',
'email' => 'alamat email',
'no_wa' => 'nomor WA',
'role' => 'peran admin',
'alamat' => 'alamat lengkap',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_modal', 'create');
}
User::create([
'nama' => $request->nama,
'username' => $request->username,
'email' => $request->email,
'no_wa' => $request->no_wa,
'role' => $request->role,
'alamat' => $request->alamat,
'password' => Hash::make($request->username),
]);
return redirect()->back()->with('success', 'Admin berhasil ditambahkan!'); return redirect()->back()->with('success', 'Admin berhasil ditambahkan!');
} }
// --- Bagian UPDATE --- public function update(AdminRequest $request, string $id)
public function update(Request $request, string $id)
{ {
$admin = User::findOrFail($id); $admin = User::findOrFail($id);
$validator = Validator::make($request->all(), [ $admin->update($request->validated());
'nama' => 'required|string|min:5|max:100',
'username' => 'required|string|alpha_dash|max:50|unique:users,username,' . $id . ',id_user',
'email' => 'required|email:dns|max:255|unique:users,email,' . $id . ',id_user',
'no_wa' => 'required|numeric|digits_between:10,15',
'role' => 'required|in:admin_foto,admin_buket',
'alamat' => 'required|string|max:255',
], [
'required' => 'Kolom :attribute wajib diisi.',
'unique' => ':attribute sudah terdaftar di sistem, gunakan yang lain.',
'min' => ':attribute minimal harus berisi :min karakter.',
'max' => ':attribute maksimal hanya boleh :max karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus berjumlah antara :min sampai :max digit.',
'email' => 'Format :attribute tidak valid.',
'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, serta simbol - dan _',
'in' => ':attribute yang dipilih tidak sesuai dengan pilihan yang tersedia.',
], [
'nama' => 'nama lengkap',
'username' => 'username',
'email' => 'alamat email',
'no_wa' => 'nomor WA',
'role' => 'peran admin',
'alamat' => 'alamat lengkap',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error_id', $id);
}
$admin->update($request->only(['nama', 'username', 'email', 'no_wa', 'role', 'alamat']));
return redirect()->back()->with('success', 'Data berhasil diperbarui!'); return redirect()->back()->with('success', 'Data berhasil diperbarui!');
} }
public function destroy(string $id) public function destroy(string $id)
{ {
// 1. Cari data admin berdasarkan ID
$admin = User::findOrFail($id); $admin = User::findOrFail($id);
// 2. Keamanan tambahan: Jangan biarkan admin menghapus dirinya sendiri (opsional)
if (Auth::id() == $admin->id_user) { if (Auth::id() == $admin->id_user) {
return redirect()->back()->with('error', 'Anda tidak bisa menghapus akun sendiri!'); return redirect()->back()->with('error', 'Anda tidak bisa menghapus akun sendiri!');
} }
// 3. Eksekusi hapus
$admin->delete(); $admin->delete();
// 4. Kembali dengan pesan sukses
return redirect()->route('admin.kelola-admin.index') return redirect()->route('admin.kelola-admin.index')
->with('success', 'Admin berhasil dihapus secara permanen.'); ->with('success', 'Admin berhasil dihapus secara permanen.');
} }

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\TransaksiBuket; use App\Models\TransaksiBuket;
use App\Services\WhatsAppService; // Import Service
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PesananBuketController extends Controller class PesananBuketController extends Controller
@ -19,61 +20,21 @@ public function index()
public function updateStatus(Request $request, $id) public function updateStatus(Request $request, $id)
{ {
try { try {
$pesanan = \App\Models\TransaksiBuket::with(['pelanggan', 'buket'])->findOrFail($id); $pesanan = TransaksiBuket::with(['pelanggan', 'buket'])->findOrFail($id);
if ($request->jenis === 'terima') { $status = $request->jenis === 'terima' ? 'diterima' : ($request->jenis === 'selesai' ? 'selesai' : 'ditolak');
$status = 'diterima'; $labels = [
session()->flash('success', "Pesanan {$pesanan->no_invoice} telah diterima!"); 'diterima' => 'telah diterima',
} elseif ($request->jenis === 'selesai') { 'selesai' => 'berhasil diselesaikan',
$status = 'selesai'; 'ditolak' => 'telah ditolak'
session()->flash('success', "Pesanan {$pesanan->no_invoice} berhasil diselesaikan!"); ];
} else { $sessionKey = $status === 'ditolak' ? 'error' : 'success';
$status = 'ditolak'; $sessionMsg = "Pesanan {$pesanan->no_invoice} " . $labels[$status] . "!";
session()->flash('error', "Pesanan {$pesanan->no_invoice} telah ditolak!"); session()->flash($sessionKey, $sessionMsg);
}
$pesanan->update(['status_transaksi' => $status]); $pesanan->update(['status_transaksi' => $status]);
$nama = $pesanan->pelanggan->nama; $waUrl = WhatsAppService::getBuketNotification($pesanan, $status);
$produk = $pesanan->buket->nama;
$tgl_obj = \Carbon\Carbon::parse($pesanan->tgl_ambil)->locale('id');
$tanggal = $tgl_obj->translatedFormat('l, d F Y');
$waktu = $tgl_obj->format('H:i');
$total = number_format($pesanan->total_bayar, 0, ',', '.');
$req = $pesanan->request ?? '-';
$ucapan = $pesanan->ucapan ?? '-';
$invoice = $pesanan->no_invoice;
$msg = null;
if ($status === 'diterima') {
$msg = "Halo Kak *{$nama}*,\n\n" .
"Pesanan Anda dengan Nomor Invoice: *{$invoice}* telah kami *TERIMA* dan masuk dalam daftar proses pengerjaan.\n\n" .
"*Rincian Pesanan:*\n" .
"- *Produk:* {$produk}\n" .
"- *Total Bayar:* Rp {$total}\n" .
"- *Request:* {$req}\n" .
"- *Ucapan:* \"{$ucapan}\"\n" .
"- *Waktu Pengambilan:* {$tanggal} pukul {$waktu} WIB\n\n" .
"Mohon simpan rincian ini dan ditunggu info selanjutnya ya Kak. Terima kasih!";
} elseif ($status === 'ditolak') {
$msg = "Halo Kak *{$nama}*,\n\n" .
"Mengenai pesanan Anda dengan Nomor Invoice: *{$invoice}* terpaksa kami *TOLAK* dikarenakan:\n\n" .
"*[TULIS ALASAN DI SINI]*\n\n" .
"*Rincian Pesanan:*\n" .
"- *Produk:* {$produk}\n" .
"- *Total Bayar:* Rp {$total}\n" .
"- *Waktu Pengambilan:* {$tanggal}\n\n" .
"Admin kami akan segera menghubungi Kakak terkait proses pengembalian dana. Mohon maaf atas ketidaknyamanannya.";
}
$wa_url = null;
if ($msg) {
$no_wa = preg_replace('/[^0-9]/', '', $pesanan->pelanggan->no_wa);
if (str_starts_with($no_wa, '0')) {
$no_wa = '62' . substr($no_wa, 1);
} elseif (str_starts_with($no_wa, '8')) {
$no_wa = '62' . $no_wa;
}
$wa_url = "https://wa.me/{$no_wa}?text=" . urlencode($msg);
}
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'wa_url' => $wa_url 'wa_url' => $waUrl
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500); return response()->json(['success' => false, 'message' => $e->getMessage()], 500);

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\BookingFoto; use App\Models\BookingFoto;
use App\Services\WhatsAppService; // Import Service
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PesananFotoController extends Controller class PesananFotoController extends Controller
@ -24,72 +25,21 @@ public function index()
public function updateStatus(Request $request, $id) public function updateStatus(Request $request, $id)
{ {
try { try {
$pesanan = \App\Models\BookingFoto::with(['pelanggan', 'paketFoto', 'detailAdditional.additional'])->findOrFail($id); $pesanan = BookingFoto::with(['pelanggan', 'paketFoto', 'detailAdditional.additional'])->findOrFail($id);
if ($request->jenis === 'terima') { $status = $request->jenis === 'terima' ? 'diterima' : ($request->jenis === 'selesai' ? 'selesai' : 'ditolak');
$status = 'diterima'; $labels = [
session()->flash('success', "Booking {$pesanan->no_invoice} telah diterima!"); 'diterima' => 'telah diterima',
} elseif ($request->jenis === 'selesai') { 'selesai' => 'berhasil diselesaikan',
$status = 'selesai'; 'ditolak' => 'telah ditolak'
session()->flash('success', "Booking {$pesanan->no_invoice} berhasil diselesaikan!"); ];
} else { $sessionKey = $status === 'ditolak' ? 'error' : 'success';
$status = 'ditolak'; $sessionMsg = "Booking {$pesanan->no_invoice} " . $labels[$status] . "!";
session()->flash('error', "Booking {$pesanan->no_invoice} telah ditolak!"); session()->flash($sessionKey, $sessionMsg);
}
$pesanan->update(['status_booking' => $status]); $pesanan->update(['status_booking' => $status]);
$nama = $pesanan->pelanggan->nama; $waUrl = WhatsAppService::getFotoNotification($pesanan, $status);
$paket = $pesanan->paketFoto->nama;
$tgl_obj = \Carbon\Carbon::parse($pesanan->tgl_booking)->locale('id');
$tanggal = $tgl_obj->translatedFormat('l, d F Y');
$jam_mulai = \Carbon\Carbon::parse($pesanan->jam_mulai)->format('H:i');
$jam_selesai = \Carbon\Carbon::parse($pesanan->jam_selesai)->format('H:i');
$total = number_format($pesanan->total_bayar, 0, ',', '.');
$invoice = $pesanan->no_invoice;
$list_additional = "";
if ($pesanan->detailAdditional->count() > 0) {
foreach ($pesanan->detailAdditional as $item) {
$list_additional .= "- " . $item->additional->nama . " (x" . $item->qty . ")\n";
}
} else {
$list_additional = "- Tidak ada additional\n";
}
$msg = null;
if ($status === 'diterima') {
$msg = "Halo Kak *{$nama}* ,\n\n" .
"Booking foto Anda dengan Invoice: *{$invoice}* telah kami *TERIMA*. \n\n" .
"*Rincian Booking:*\n" .
"- *Paket:* {$paket}\n" .
"*Additional:*\n{$list_additional}" .
"- *Total Bayar:* Rp {$total}\n\n" .
"*Jadwal Sesi Foto:*\n" .
" Tanggal: {$tanggal}\n" .
" Jam: {$jam_mulai} - {$jam_selesai} WIB\n\n" .
"Mohon datang 15 menit sebelum jadwal dimulai ya Kak. Sampai jumpa di studio! ";
} elseif ($status === 'ditolak') {
$msg = "Halo Kak *{$nama}*,\n\n" .
"Mohon maaf, booking foto Anda dengan Invoice *{$invoice}* terpaksa kami *TOLAK* dikarenakan:\n\n" .
"*[TULIS ALASAN DI SINI]*\n\n" .
"*Rincian Booking:*\n" .
"- *Paket:* {$paket}\n" .
"*Additional:*\n{$list_additional}" .
"- *Total Bayar:* Rp {$total}\n" .
"*Jadwal Sesi Foto:*\n" .
" Tanggal: {$tanggal}\n" .
" Jam: {$jam_mulai} - {$jam_selesai} WIB\n\n" .
"Admin kami akan segera menghubungi Kakak untuk info pengembalian dana atau penjadwalan ulang. Terima kasih. ";
}
$wa_url = null;
if ($msg) {
$no_wa = preg_replace('/[^0-9]/', '', $pesanan->pelanggan->no_wa);
if (str_starts_with($no_wa, '0')) {
$no_wa = '62' . substr($no_wa, 1);
} elseif (str_starts_with($no_wa, '8')) {
$no_wa = '62' . $no_wa;
}
$wa_url = "https://wa.me/{$no_wa}?text=" . urlencode($msg);
}
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'wa_url' => $wa_url 'wa_url' => $waUrl
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500); return response()->json(['success' => false, 'message' => $e->getMessage()], 500);

View File

@ -3,16 +3,13 @@
namespace App\Http\Controllers\User; namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Additional; use App\Models\{Additional, BookingFoto, DetailAdditional, PaketFoto, Pelanggan};
use App\Models\BookingFoto; use App\Http\Requests\User\BookingFotoRequest;
use App\Models\DetailAdditional; use App\Services\WhatsAppService;
use App\Models\PaketFoto;
use App\Models\Pelanggan;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage; use Carbon\Carbon;
use Illuminate\Support\Facades\Validator;
class BookingFotoController extends Controller class BookingFotoController extends Controller
@ -64,204 +61,87 @@ public function cekSlot(Request $request)
} }
public function formulir(Request $request) public function formulir(Request $request)
{ {
$foto = PaketFoto::findOrFail($request->id_paket); $details = $this->calculateDetails($request);
$durasiDasar = $foto->durasi;
$addonsDetails = [];
$totalAddon = 0;
$tambahanMenit = 0;
if ($request->has('addons')) {
foreach ($request->addons as $id => $qty) {
if ($qty > 0) {
$add = Additional::find($id);
if ($add) {
$subtotal = $add->harga * $qty;
$totalAddon += $subtotal;
if ($id == 4) {
$tambahanMenit += (5 * $qty);
} elseif ($id == 6) {
$tambahanMenit += (10 * $qty);
}
$addonsDetails[] = [
'nama' => $add->nama,
'qty' => $qty,
'subtotal' => $subtotal,
'id' => $id
];
}
}
}
}
$totalDurasi = $durasiDasar + $tambahanMenit;
$jamMulai = \Carbon\Carbon::createFromFormat('H:i', $request->jam_mulai);
$jamSelesaiBaru = $jamMulai->copy()->addMinutes($totalDurasi)->format('H:i');
$grandTotal = $foto->harga + $totalAddon;
if (!session()->has('payment_deadline')) { if (!session()->has('payment_deadline')) {
$deadline = now()->addHours(2); session()->put('payment_deadline', now()->addHours(2));
session()->put('payment_deadline', $deadline);
} else {
$deadline = session('payment_deadline');
} }
$sisaWaktu = now()->diffInSeconds($deadline, false); $sisaWaktu = now()->diffInSeconds(session('payment_deadline'), false);
if ($sisaWaktu <= 0) { if ($sisaWaktu <= 0) {
session()->forget(['payment_deadline', 'addons']); session()->forget(['payment_deadline', 'addons']);
return redirect()->route('booking.foto')->with('error', 'Waktu pembayaran telah habis.'); return redirect()->route('booking.foto')->with('error', 'Waktu pembayaran telah habis.');
} }
return view('user.pembayaran-foto', compact( return view('user.pembayaran-foto', array_merge($details, [
'foto', 'sisaWaktu' => $sisaWaktu,
'request', 'request' => $request
'addonsDetails', ]));
'grandTotal',
'sisaWaktu',
'jamSelesaiBaru'
));
} }
public function cancelBooking() public function cancelBooking()
{ {
session()->forget(['payment_deadline', 'addons']); session()->forget(['payment_deadline', 'addons']);
return redirect()->route('booking.foto'); return redirect()->route('booking.foto');
} }
public function store(Request $request) public function store(BookingFotoRequest $request)
{ {
$validator = Validator::make($request->all(), [ $details = $this->calculateDetails($request);
'id_paket' => 'required|exists:paket_fotos,id_paket', return DB::transaction(function () use ($request, $details) {
'tgl_booking' => 'required|date|after_or_equal:today', if (BookingFoto::where(['tgl_booking' => $request->tgl_booking, 'jam_mulai' => $request->jam_mulai])
'jam_mulai' => 'required', ->whereIn('status_booking', ['menunggu_verifikasi', 'diterima'])->exists()
'nama' => 'required|string|min:3|max:100', ) {
'no_wa' => 'required|numeric|digits_between:10,15', return back()->with('error', 'Slot baru saja diambil orang lain.');
'bukti_bayar' => 'required|image|mimes:jpeg,png,jpg|max:2048',
], [
'required' => 'Kolom :attribute wajib diisi.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal :min karakter.',
'max' => ':attribute terlalu panjang, maksimal :max karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus antara :min sampai :max digit.',
'date' => 'Format tanggal pada :attribute tidak valid.',
'after_or_equal' => ':attribute tidak boleh tanggal yang sudah lewat.',
'image' => ':attribute harus berupa file gambar.',
'mimes' => 'Format :attribute harus jpeg, png, atau jpg.',
'bukti_bayar.max' => 'Ukuran :attribute maksimal adalah 2MB.',
], [
'nama' => 'nama pemesan',
'no_wa' => 'nomor WhatsApp',
'bukti_bayar' => 'bukti pembayaran',
]);
DB::beginTransaction();
try {
$paket = PaketFoto::findOrFail($request->id_paket);
$durasiMenit = $paket->durasi;
$jamMulai = \Carbon\Carbon::createFromFormat('H:i', $request->jam_mulai);
$jamSelesai = $jamMulai->copy()->addMinutes($durasiMenit);
$isTaken = BookingFoto::where('tgl_booking', $request->tgl_booking)
->where('jam_mulai', $request->jam_mulai)
->whereIn('status_booking', ['menunggu_verifikasi', 'diterima', 'selesai'])
->exists();
if ($isTaken) {
return back()->with('error', 'Mohon maaf, slot waktu ini baru saja diambil orang lain.');
} }
$pelanggan = Pelanggan::firstOrCreate(['no_wa' => $request->no_wa], ['nama' => $request->nama]);
$pelanggan = Pelanggan::firstOrCreate( $pathBukti = $request->hasFile('bukti_bayar')
['no_wa' => $request->no_wa], ? $request->file('bukti_bayar')->store('img/payment/foto', 'public')
['nama' => $request->nama] : null;
); $booking = BookingFoto::create([
$grandTotal = $paket->harga; 'no_invoice' => 'INV-FOTO-' . strtoupper(Str::random(6)),
$listAdditional = []; 'id_pelanggan' => $pelanggan->id_pelanggan,
$totalDurasi = $durasiMenit; 'id_paket' => $details['foto']->id_paket,
if ($request->has('addons')) { 'tgl_booking' => $request->tgl_booking,
foreach ($request->addons as $idAddon => $qty) { 'jam_mulai' => $request->jam_mulai,
if ($qty > 0) { 'jam_selesai' => $details['jamSelesaiBaru'],
$add = \App\Models\Additional::find($idAddon); 'total_bayar' => $details['grandTotal'],
if ($add) { 'bukti_bayar' => $pathBukti,
$subtotal = $add->harga * $qty;
$grandTotal += $subtotal;
if ($idAddon == 4) $totalDurasi += (5 * $qty);
if ($idAddon == 6) $totalDurasi += (10 * $qty);
$listAdditional[] = [
'id_additional' => $idAddon,
'qty' => $qty,
'subtotal' => $subtotal,
'nama_barang' => $add->nama
];
}
}
}
}
$jamMulai = \Carbon\Carbon::createFromFormat('H:i', $request->jam_mulai);
$jamSelesai = $jamMulai->copy()->addMinutes($totalDurasi);
$isTaken = \App\Models\BookingFoto::where('tgl_booking', $request->tgl_booking)
->where('jam_mulai', $request->jam_mulai)
->whereIn('status_booking', ['menunggu_verifikasi', 'diterima'])
->exists();
if ($isTaken) {
return back()->with('error', 'Mohon maaf, slot waktu ini baru saja diambil orang lain.');
}
$pelanggan = \App\Models\Pelanggan::create([
'nama' => $request->nama,
'no_wa' => $request->no_wa
]);
$pathBukti = null;
if ($request->hasFile('bukti_bayar')) {
$file = $request->file('bukti_bayar');
$namaFile = 'bukti_' . time() . '.' . $file->getClientOriginalExtension();
$pathBukti = $file->storeAs('img/payment/foto', $namaFile, 'public');
}
$booking = \App\Models\BookingFoto::create([
'no_invoice' => 'INV-FOTO-' . strtoupper(\Illuminate\Support\Str::random(6)),
'id_pelanggan' => $pelanggan->id_pelanggan,
'id_paket' => $paket->id_paket,
'tgl_booking' => $request->tgl_booking,
'jam_mulai' => $request->jam_mulai,
'jam_selesai' => $jamSelesai->format('H:i'),
'total_bayar' => $grandTotal,
'bukti_bayar' => $pathBukti,
'status_booking' => 'menunggu_verifikasi' 'status_booking' => 'menunggu_verifikasi'
]); ]);
foreach ($listAdditional as $item) { foreach ($details['addonsDetails'] as $item) {
\App\Models\DetailAdditional::create([ DetailAdditional::create([
'id_booking' => $booking->id_booking, 'id_booking' => $booking->id_booking,
'id_additional' => $item['id_additional'], 'id_additional' => $item['id'],
'qty' => $item['qty'], 'qty' => $item['qty'],
'subtotal' => $item['subtotal'] 'subtotal' => $item['subtotal']
]); ]);
} }
\Illuminate\Support\Facades\DB::commit(); $urlWA = WhatsAppService::getBookingFotoMessage($booking, $details['foto'], $details['addonsDetails'], $details['grandTotal'], $details['jamSelesaiBaru']);
$txtAddons = "";
if (count($listAdditional) > 0) {
$txtAddons = "*Additional:*";
foreach ($listAdditional as $item) {
$txtAddons .= "\n- " . $item['nama_barang'] . " (" . $item['qty'] . "x)";
}
$txtAddons .= "\n";
}
$pesan = "Halo Admin Flo.do! Saya sudah melakukan pembayaran untuk invoice {$booking->no_invoice}:\n\n" .
"*Data Pemesan:*\n" .
"Nama: {$request->nama}\n" .
"WA: {$request->no_wa}\n\n" .
"*Detail Booking:*\n" .
"Nama Paket: {$paket->nama}\n" .
$txtAddons .
"Tanggal: " . \Carbon\Carbon::parse($request->tgl_booking)->translatedFormat('l, d F Y') . "\n" .
"Jam: {$request->jam_mulai} - {$jamSelesai->format('H:i')} WIB\n" .
"Total: Rp " . number_format($grandTotal, 0, ',', '.') . "\n\n" .
"Mohon segera diproses, ya! Terima kasih.";
$urlWA = "https://wa.me/6282337687878?text=" . urlencode($pesan);
session()->forget('payment_deadline'); session()->forget('payment_deadline');
return redirect()->route('booking.foto')->with([ return redirect()->route('booking.foto')->with(['success' => 'Pesanan Berhasil!', 'waUrl' => $urlWA]);
'success' => 'Pesanan Berhasil Dibuat!', });
'waUrl' => $urlWA }
]); private function calculateDetails($request)
} catch (\Exception $e) { {
\Illuminate\Support\Facades\DB::rollBack(); $foto = PaketFoto::findOrFail($request->id_paket);
return back()->with('error', 'Error: ' . $e->getMessage())->withInput(); $totalAddon = 0;
$tambahanMenit = 0;
$addonsDetails = [];
if ($request->has('addons')) {
foreach ($request->addons as $id => $qty) {
if ($qty > 0 && $add = Additional::find($id)) {
$subtotal = $add->harga * $qty;
$totalAddon += $subtotal;
if ($id == 4) $tambahanMenit += (5 * $qty);
if ($id == 6) $tambahanMenit += (10 * $qty);
$addonsDetails[] = ['nama' => $add->nama, 'qty' => $qty, 'subtotal' => $subtotal, 'id' => $id];
}
}
} }
return [
'foto' => $foto,
'addonsDetails' => $addonsDetails,
'grandTotal' => $foto->harga + $totalAddon,
'jamSelesaiBaru' => Carbon::createFromFormat('H:i', $request->jam_mulai)
->addMinutes($foto->durasi + $tambahanMenit)->format('H:i')
];
} }
} }

View File

@ -10,6 +10,8 @@
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use App\Http\Requests\User\PesanBuketRequest;
use App\Services\WhatsAppService;
class PesanBuketController extends Controller class PesanBuketController extends Controller
{ {

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class AdditionalRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Izinkan akses request
}
public function rules(): array
{
return [
'nama' => 'required|string|min:3|max:100',
'harga' => 'required|numeric|min:0',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute wajib diisi.',
'numeric' => ':attribute harus berupa angka.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'max' => ':attribute melebihi batas maksimal :max karakter.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama additional',
'harga' => 'harga additional',
];
}
/**
* Mengatur respon jika validasi gagal agar modal tidak tertutup.
*/
protected function failedValidation(Validator $validator)
{
$redirect = redirect()->back()->withErrors($validator)->withInput();
if ($this->isMethod('post')) {
// Untuk modal tambah additional
$redirect->with('error_modal', 'createAdd');
} else {
// Untuk modal edit berdasarkan ID additional
$redirect->with('error_id_add', $this->route('additional'));
}
throw new HttpResponseException($redirect);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class AdminRequest extends FormRequest
{
public function authorize(): bool
{
// Pastikan ini true agar request diproses
return true;
}
public function rules(): array
{
// Mengambil ID dari route untuk pengecualian 'unique' saat update
$adminId = $this->route('id');
return [
'nama' => 'required|string|min:5|max:100',
'username' => 'required|string|alpha_dash|max:50|unique:users,username,' . $adminId . ',id_user',
'email' => 'required|email:dns|max:255|unique:users,email,' . $adminId . ',id_user',
'no_wa' => 'required|numeric|digits_between:10,15',
'role' => 'required|in:admin_foto,admin_buket',
'alamat' => 'required|string|max:255',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute wajib diisi.',
'unique' => ':attribute sudah terdaftar di sistem, gunakan yang lain.',
'min' => ':attribute minimal harus berisi :min karakter.',
'max' => ':attribute maksimal hanya boleh :max karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus berjumlah antara :min sampai :max digit.',
'email' => 'Format :attribute tidak valid.',
'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, serta simbol - dan _',
'in' => ':attribute yang dipilih tidak sesuai pilihan yang tersedia.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama lengkap',
'username' => 'username',
'email' => 'alamat email',
'no_wa' => 'nomor WA',
'role' => 'peran admin',
'alamat' => 'alamat lengkap',
];
}
/**
* Penanganan khusus agar Modal tetap terbuka saat validasi gagal.
*/
protected function failedValidation(Validator $validator)
{
$redirect = redirect()->back()->withErrors($validator)->withInput();
if ($this->isMethod('post')) {
$redirect->with('error_modal', 'create');
} else {
$redirect->with('error_id', $this->route('id'));
}
throw new HttpResponseException($redirect);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class BuketRequest extends FormRequest
{
public function authorize(): bool
{
// Ubah jadi true supaya request diizinkan
return true;
}
public function rules(): array
{
return [
'nama' => 'required|string|min:3|max:100',
'ukuran' => 'required|in:S,M,L',
'kategori' => 'required|in:single,fresh,premium_fresh,artificial',
'harga' => 'required|numeric|min:0',
'request_khusus' => 'nullable|string|max:255',
'deskripsi' => 'required|string|min:10',
// Dinamis: required jika store (POST), nullable jika update (PUT)
'foto' => $this->isMethod('post')
? 'required|image|mimes:jpeg,png,jpg|max:2048'
: 'nullable|image|mimes:jpeg,png,jpg|max:2048',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute tidak boleh kosong.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal harus :min karakter.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'in' => 'Pilihan :attribute tidak sesuai dengan data yang tersedia.',
'numeric' => ':attribute harus berupa angka.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung. Gunakan format: jpeg, png, atau jpg.',
'foto.max' => 'Ukuran :attribute terlalu besar, maksimal adalah 2MB.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama buket',
'ukuran' => 'ukuran buket',
'kategori' => 'kategori buket',
'harga' => 'harga',
'request_khusus' => 'request khusus',
'deskripsi' => 'deskripsi produk',
'foto' => 'foto produk',
];
}
/**
* Trik untuk Modal: Mengembalikan error ke modal yang tepat
*/
protected function failedValidation(Validator $validator)
{
$redirect = redirect()->back()->withErrors($validator)->withInput();
if ($this->isMethod('post')) {
$redirect->with('error_modal', 'create');
} else {
// Mengambil ID dari parameter route untuk modal edit
$redirect->with('error_id', $this->route('buket'));
}
throw new HttpResponseException($redirect);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class FotoRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Izinkan request
}
public function rules(): array
{
return [
'nama' => 'required|string|min:3|max:100',
'harga' => 'required|numeric|min:0',
'durasi' => 'required|integer|min:0',
'deskripsi' => 'required|string|min:10',
// Foto wajib saat Tambah (POST), opsional saat Edit (PUT)
'foto' => $this->isMethod('post')
? 'required|image|mimes:jpeg,png,jpg|max:2048'
: 'nullable|image|mimes:jpeg,png,jpg|max:2048',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute wajib diisi.',
'string' => 'Input :attribute harus berupa teks valid.',
'integer' => 'Input :attribute harus berupa angka valid.',
'numeric' => ':attribute harus berupa angka.',
'min' => ':attribute minimal harus :min karakter/nilai.',
'max' => ':attribute melebihi batas, maksimal :max karakter/KB.',
'image' => ':attribute harus berupa file gambar (foto).',
'mimes' => 'Format :attribute tidak didukung (jpeg, png, jpg).',
'foto.max' => 'Ukuran :attribute maksimal 2MB.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama paket',
'harga' => 'harga paket',
'durasi' => 'durasi paket',
'foto' => 'foto paket',
];
}
/**
* Penanganan khusus untuk Modal di Admin
*/
protected function failedValidation(Validator $validator)
{
$redirect = redirect()->back()->withErrors($validator)->withInput();
if ($this->isMethod('post')) {
// Trigger modal tambah foto
$redirect->with('error_modal', 'createFoto');
} else {
// Trigger modal edit berdasarkan ID paket
$redirect->with('error_id_foto', $this->route('foto'));
}
throw new HttpResponseException($redirect);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ProfilRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class BookingFotoRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'id_paket' => 'required|exists:paket_fotos,id_paket',
'tgl_booking' => 'required|date|after_or_equal:today',
'jam_mulai' => 'required',
'nama' => 'required|string|min:3|max:100',
'no_wa' => 'required|numeric|digits_between:10,15',
'bukti_bayar' => 'required|image|mimes:jpeg,png,jpg|max:2048',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute wajib diisi.',
'image' => ':attribute harus berupa file gambar.',
'max' => 'Ukuran :attribute maksimal adalah 2MB.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal :min karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus antara :min sampai :max digit.',
'date' => 'Format tanggal pada :attribute tidak valid.',
'after_or_equal' => ':attribute tidak boleh tanggal yang sudah lewat.',
'mimes' => 'Format :attribute harus jpeg, png, atau jpg.',
'bukti_bayar.max' => 'Ukuran :attribute maksimal adalah 2MB.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama pemesan',
'no_wa' => 'nomor WhatsApp',
'bukti_bayar' => 'bukti pembayaran',
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class PesanBuketRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Pastikan ini true agar request diizinkan
}
public function rules(): array
{
return [
'id_buket' => 'required|exists:bukets,id_buket', // Tambahan validasi keamanan
'nama' => 'required|string|min:3|max:100',
'no_wa' => 'required|numeric|digits_between:10,15',
'tgl_ambil' => 'required|date|after_or_equal:today',
'waktu_ambil' => [
'required',
'date_format:H:i',
'after_or_equal:09:00',
'before_or_equal:21:00',
],
'bukti_bayar' => 'required|image|mimes:jpeg,png,jpg|max:2048',
'request_khusus' => 'nullable|string|max:255',
'ucapan' => 'nullable|string|max:500',
];
}
public function messages(): array
{
return [
'required' => 'Kolom :attribute wajib diisi.',
'string' => 'Input :attribute harus berupa teks valid.',
'min' => ':attribute terlalu pendek, minimal :min karakter.',
'max' => ':attribute terlalu panjang, maksimal :max karakter.',
'numeric' => ':attribute harus berupa angka.',
'digits_between' => ':attribute harus antara :min sampai :max digit.',
'date' => 'Format tanggal pada :attribute tidak valid.',
'after_or_equal' => ':attribute tidak boleh tanggal yang sudah lewat.',
'image' => ':attribute harus berupa file gambar.',
'mimes' => 'Format :attribute harus jpeg, png, atau jpg.',
'bukti_bayar.max' => 'Ukuran :attribute maksimal adalah 2MB.',
'waktu_ambil.after_or_equal' => 'Jam operasional kami mulai pukul 09:00.',
'waktu_ambil.before_or_equal' => 'Jam operasional kami berakhir pukul 21:00.',
];
}
public function attributes(): array
{
return [
'nama' => 'nama pemesan',
'no_wa' => 'nomor WhatsApp',
'tgl_ambil' => 'tanggal pengambilan',
'waktu_ambil' => 'waktu pengambilan',
'bukti_bayar' => 'bukti pembayaran',
'ucapan' => 'kartu ucapan',
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Services;
use App\Models\{TransaksiBuket, BookingFoto};
use Carbon\Carbon;
class DashboardService
{
public static function getStats()
{
$now = Carbon::now();
$prev = Carbon::now()->subMonth();
$curr = self::calculate($now);
$past = self::calculate($prev);
return [
'pendapatan' => $curr['income'],
'pendapatan_grow' => self::growth($curr['income'], $past['income']),
'masuk_count' => $curr['total'],
'masuk_grow' => self::growth($curr['total'], $past['total']),
'selesai_count' => $curr['done'],
'selesai_grow' => self::growth($curr['done'], $past['done']),
'batal_count' => $curr['cancel'],
'batal_grow' => self::growth($curr['cancel'], $past['cancel']),
];
}
private static function calculate($date)
{
$m = $date->month;
$y = $date->year;
// Gabungan query Buket & Foto
return [
'income' => TransaksiBuket::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_transaksi', 'diterima')->sum('total_bayar') +
BookingFoto::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_booking', 'diterima')->sum('total_bayar'),
'total' => TransaksiBuket::whereMonth('created_at', $m)->whereYear('created_at', $y)->count() +
BookingFoto::whereMonth('created_at', $m)->whereYear('created_at', $y)->count(),
'done' => TransaksiBuket::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_transaksi', 'selesai')->count() +
BookingFoto::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_booking', 'selesai')->count(),
'cancel' => TransaksiBuket::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_transaksi', 'ditolak')->count() +
BookingFoto::whereMonth('created_at', $m)->whereYear('created_at', $y)->where('status_booking', 'ditolak')->count(),
];
}
private static function growth($current, $previous)
{
if ($previous <= 0) return $current > 0 ? 100 : 0;
return round((($current - $previous) / $previous) * 100, 1);
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace App\Services;
use Carbon\Carbon;
class WhatsAppService
{
public static function getBuketNotification($pesanan, $status)
{
if ($status === 'selesai') return null;
$nama = $pesanan->pelanggan->nama;
$produk = $pesanan->buket->nama;
$invoice = $pesanan->no_invoice;
$total = number_format($pesanan->total_bayar, 0, ',', '.');
$req = $pesanan->request ?? '-';
$ucapan = $pesanan->ucapan ?? '-';
$tgl_obj = Carbon::parse($pesanan->tgl_ambil)->locale('id');
$tanggal = $tgl_obj->translatedFormat('l, d F Y');
$waktu = $tgl_obj->format('H:i');
$msg = self::getBuketTemplate($status, $nama, $invoice, $produk, $total, $req, $ucapan, $tanggal, $waktu);
$no_wa = self::formatPhoneNumber($pesanan->pelanggan->no_wa);
return "https://wa.me/{$no_wa}?text=" . urlencode($msg);
}
private static function getBuketTemplate($status, $nama, $invoice, $produk, $total, $req, $ucapan, $tanggal, $waktu)
{
if ($status === 'diterima') {
return "Halo Kak *{$nama}*,\n\n" .
"Pesanan Anda dengan Nomor Invoice: *{$invoice}* telah kami *TERIMA* dan masuk dalam daftar proses pengerjaan.\n\n" .
"*Rincian Pesanan:*\n" .
"- *Produk:* {$produk}\n" .
"- *Total Bayar:* Rp {$total}\n" .
"- *Request:* {$req}\n" .
"- *Ucapan:* \"{$ucapan}\"\n" .
"- *Waktu Pengambilan:* {$tanggal} pukul {$waktu} WIB\n\n" .
"Mohon simpan rincian ini dan ditunggu info selanjutnya ya Kak. Terima kasih!";
}
return "Halo Kak *{$nama}*,\n\n" .
"Mengenai pesanan Anda dengan Nomor Invoice: *{$invoice}* terpaksa kami *TOLAK* dikarenakan:\n\n" .
"*[TULIS ALASAN DI SINI]*\n\n" .
"*Rincian Pesanan:*\n" .
"- *Produk:* {$produk}\n" .
"- *Total Bayar:* Rp {$total}\n" .
"- *Waktu Pengambilan:* {$tanggal}\n\n" .
"Admin kami akan segera menghubungi Kakak terkait proses pengembalian dana. Mohon maaf atas ketidaknyamanannya.";
}
private static function formatPhoneNumber($number)
{
$number = preg_replace('/[^0-9]/', '', $number);
if (str_starts_with($number, '0')) {
return '62' . substr($number, 1);
} elseif (str_starts_with($number, '8')) {
return '62' . $number;
}
return $number;
}
public static function getFotoNotification($pesanan, $status)
{
if ($status === 'selesai') return null;
$nama = $pesanan->pelanggan->nama;
$paket = $pesanan->paketFoto->nama;
$invoice = $pesanan->no_invoice;
$total = number_format($pesanan->total_bayar, 0, ',', '.');
// Format Tanggal & Jam
$tgl_obj = Carbon::parse($pesanan->tgl_booking)->locale('id');
$tanggal = $tgl_obj->translatedFormat('l, d F Y');
$jam = Carbon::parse($pesanan->jam_mulai)->format('H:i') . ' - ' . Carbon::parse($pesanan->jam_selesai)->format('H:i');
// Logika List Additional
$list_additional = $pesanan->detailAdditional->count() > 0
? $pesanan->detailAdditional->map(fn($item) => "- " . $item->additional->nama . " (x" . $item->qty . ")")->implode("\n")
: "- Tidak ada additional";
// Pilih Template
$msg = self::getFotoTemplate($status, $nama, $invoice, $paket, $list_additional, $total, $tanggal, $jam);
// Format Nomor HP
$no_wa = self::formatPhoneNumber($pesanan->pelanggan->no_wa);
return "https://wa.me/{$no_wa}?text=" . urlencode($msg);
}
private static function getFotoTemplate($status, $nama, $invoice, $paket, $additional, $total, $tanggal, $jam)
{
if ($status === 'diterima') {
return "Halo Kak *{$nama}*,\n\n" .
"Booking foto Anda dengan Invoice: *{$invoice}* telah kami *TERIMA*. \n\n" .
"*Rincian Booking:*\n" .
"- *Paket:* {$paket}\n" .
"*Additional:*\n{$additional}\n" .
"- *Total Bayar:* Rp {$total}\n\n" .
"*Jadwal Sesi Foto:*\n" .
" Tanggal: {$tanggal}\n" .
" Jam: {$jam} WIB\n\n" .
"Mohon datang 15 menit sebelum jadwal dimulai ya Kak. Sampai jumpa di studio!";
}
return "Halo Kak *{$nama}*,\n\n" .
"Mohon maaf, booking foto Anda dengan Invoice *{$invoice}* terpaksa kami *TOLAK* dikarenakan:\n\n" .
"*[TULIS ALASAN DI SINI]*\n\n" .
"*Rincian Booking:*\n" .
"- *Paket:* {$paket}\n" .
"*Additional:*\n{$additional}\n" .
"- *Total Bayar:* Rp {$total}\n" .
"*Jadwal Sesi Foto:*\n" .
" Tanggal: {$tanggal}\n" .
" Jam: {$jam} WIB\n\n" .
"Admin kami akan segera menghubungi Kakak untuk info pengembalian dana atau penjadwalan ulang. Terima kasih.";
}
public static function getPesananBuketMessage($transaksi, $pelanggan)
{
$total = number_format($transaksi->buket->harga, 0, ',', '.');
$pesan = "Halo Admin! Saya sudah melakukan pembayaran untuk invoice *{$transaksi->no_invoice}*:\n\n" .
"*Data Pemesan:*\n" .
"- Nama: {$pelanggan->nama}\n" .
"- WA: {$pelanggan->no_wa}\n\n" .
"*Detail Produk:*\n" .
"- Produk: {$transaksi->buket->nama}\n" .
"- Total: Rp {$total}\n\n" .
"Mohon segera diproses, ya! Terima kasih.";
return "https://wa.me/6282337687878?text=" . urlencode($pesan);
}
public static function getBookingFotoMessage($booking, $paket, $addons, $grandTotal, $jamSelesai)
{
$txtAddons = "";
if (count($addons) > 0) {
$txtAddons = "\n*Additional:*";
foreach ($addons as $item) {
$txtAddons .= "\n- " . $item['nama'] . " (" . $item['qty'] . "x)";
}
}
$pesan = "Halo Admin Flo.do! Saya sudah melakukan pembayaran untuk invoice *{$booking->no_invoice}*:\n\n" .
"*Data Pemesan:*\n- Nama: {$booking->pelanggan->nama}\n- WA: {$booking->pelanggan->no_wa}\n\n" .
"*Detail Booking:*\n- Paket: {$paket->nama}{$txtAddons}\n" .
"- Tanggal: " . \Carbon\Carbon::parse($booking->tgl_booking)->translatedFormat('l, d F Y') . "\n" .
"- Jam: {$booking->jam_mulai} - {$jamSelesai} WIB\n" .
"- Total: Rp " . number_format($grandTotal, 0, ',', '.') . "\n\n" .
"Mohon segera diproses, ya! Terima kasih.";
return "https://wa.me/6282337687878?text=" . urlencode($pesan);
}
}

View File

@ -20,115 +20,39 @@
@endif @endif
<section class="row gy-3"> <section class="row gy-3">
<div class="col-12"> <div class="col-12">
<div class="row gx-3"> @if (Auth::user()->role === 'pemilik')
@if (Auth::user()->role === 'pemilik') <div class="row">
<div class="col-6 col-lg-3 col-md-6"> @include('admin.components._stat_card', [
<div class="card mb-0"> 'label' => 'Pendapatan',
<div class="card-body px-3 py-4"> 'icon' => 'bi-bank',
<div class="stat-header"> 'value' => 'Rp ' . number_format($stat['pendapatan'], 0, ',', '.'),
<h6 class="stat-label">Pendapatan Bulan Ini</h6> 'grow' => $stat['pendapatan_grow'],
<i class="bi bi-info-circle menu-dots" data-bs-toggle="tooltip" data-bs-placement="top" 'tooltip' => 'Total pendapatan dari pesanan buket dan foto bulan ini',
title="Total pendapatan dari pesanan buket dan foto dalam bulan ini"></i> ])
</div> @include('admin.components._stat_card', [
<div class="stat-body"> 'label' => 'Pesanan Masuk',
<div class="stat-icon"> 'icon' => 'bi-cart-fill',
<i class="bi bi-bank"></i> 'value' => $stat['masuk_count'] . ' Pesanan',
</div> 'grow' => $stat['masuk_grow'],
<div> 'tooltip' => 'Total pesanan buket dan foto yang masuk bulan ini',
<h6 class="stat-count">Rp {{ number_format($stat['pendapatan'], 0, ',', '.') }}</h6> ])
<span @include('admin.components._stat_card', [
class="stat-percent {{ $stat['pendapatan_grow'] >= 0 ? 'text-success' : 'text-danger' }} fw-bold"> 'label' => 'Pesanan Selesai',
<i 'icon' => 'bi-cart-check-fill',
class="bi {{ $stat['pendapatan_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i> 'value' => $stat['selesai_count'] . ' Pesanan',
{{ abs($stat['pendapatan_grow']) }}% 'grow' => $stat['selesai_grow'],
</span> 'tooltip' => 'Total pesanan buket dan foto yang selesai bulan ini',
<small class="stat-month">dari bulan lalu</small> ])
</div> @include('admin.components._stat_card', [
</div> 'label' => 'Pesanan Ditolak',
</div> 'icon' => 'bi-cart-x-fill',
</div> 'value' => $stat['batal_count'] . ' Pesanan',
</div> 'grow' => $stat['batal_grow'],
'tooltip' => 'Total pesanan buket dan foto yang ditolak bulan ini',
<div class="col-6 col-lg-3 col-md-6"> 'is_negative_metric' => true,
<div class="card mb-0"> ])
<div class="card-body px-3 py-4"> </div>
<div class="stat-header"> @endif
<h6 class="stat-label">Total Pesanan Masuk</h6>
<i class="bi bi-info-circle menu-dots" data-bs-toggle="tooltip" data-bs-placement="top"
title="Total pesanan buket dan foto yang masuk dalam bulan ini"></i>
</div>
<div class="stat-body">
<div class="stat-icon">
<i class="bi bi-cart-fill"></i>
</div>
<div>
<h6 class="stat-count">{{ $stat['masuk_count'] }} Pesanan</h6>
<span
class="stat-percent {{ $stat['masuk_grow'] >= 0 ? 'text-success' : 'text-danger' }} fw-bold">
<i
class="bi {{ $stat['masuk_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
{{ abs($stat['masuk_grow']) }}%
</span>
<small class="stat-month">dari bulan lalu</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3 col-md-6">
<div class="card mb-0">
<div class="card-body px-3 py-4">
<div class="stat-header">
<h6 class="stat-label">Pesanan Selesai</h6>
<i class="bi bi-info-circle menu-dots" data-bs-toggle="tooltip" data-bs-placement="top"
title="Total pesanan buket dan foto yang selesai dalam bulan ini"></i>
</div>
<div class="stat-body">
<div class="stat-icon">
<i class="bi bi-cart-check-fill"></i>
</div>
<div>
<h6 class="stat-count">{{ $stat['selesai_count'] }} Pesanan</h6>
<span
class="stat-percent {{ $stat['selesai_grow'] >= 0 ? 'text-success' : 'text-danger' }} fw-bold">
<i
class="bi {{ $stat['selesai_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>{{ abs($stat['selesai_grow']) }}%
</span>
<small class="stat-month">dari bulan lalu</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3 col-md-6">
<div class="card mb-0">
<div class="card-body px-3 py-4">
<div class="stat-header">
<h6 class="stat-label">Pesanan Ditolak</h6>
<i class="bi bi-info-circle menu-dots" data-bs-toggle="tooltip" data-bs-placement="top"
title="Total pesanan buket dan foto yang ditolak dalam bulan ini"></i>
</div>
<div class="stat-body">
<div class="stat-icon">
<i class="bi bi-cart-x-fill"></i>
</div>
<div>
<h6 class="stat-count">{{ $stat['batal_count'] }} Pesanan</h6>
<span
class="stat-percent {{ $stat['batal_grow'] <= 0 ? 'text-success' : 'text-danger' }} fw-bold">
<i
class="bi {{ $stat['batal_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
{{ abs($stat['batal_grow']) }}%
</span>
<small class="stat-month">dari bulan lalu</small>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="row gx-3"> <div class="row gx-3">
@ -201,8 +125,7 @@ class="bi {{ $stat['batal_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
<td>{{ $p->paketFoto->nama }}</td> <td>{{ $p->paketFoto->nama }}</td>
<td class="col-auto"> <td class="col-auto">
<a href="#" class="btn icon btn-primary btn-action" <a href="#" class="btn icon btn-primary btn-action"
data-bs-toggle="modal" data-bs-toggle="modal" data-bs-target="#foto{{ $p->id_booking }}">
data-bs-target="#foto{{ $p->id_booking }}">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
</td> </td>
@ -229,8 +152,7 @@ class="bi {{ $stat['batal_grow'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="nav nav-pills nav-fill mb-4" id="v-pills-tab" role="tablist" <div class="nav nav-pills nav-fill mb-4" id="v-pills-tab" role="tablist" aria-orientation="horizontal">
aria-orientation="horizontal">
<a class="nav-link active" id="v-pills-home-tab" data-bs-toggle="pill" href="#v-pills-home" <a class="nav-link active" id="v-pills-home-tab" data-bs-toggle="pill" href="#v-pills-home"
role="tab" aria-controls="v-pills-home" aria-selected="true"> role="tab" aria-controls="v-pills-home" aria-selected="true">
Buket Buket

View File

@ -0,0 +1,44 @@
<div class="col-6 col-lg-3 col-md-6">
<div class="card mb-0">
<div class="card-body px-3 py-4">
<div class="stat-header">
<h6 class="stat-label">{{ $label }}</h6>
<i class="bi bi-info-circle menu-dots" data-bs-toggle="tooltip" title="{{ $tooltip }}"></i>
</div>
<div class="stat-body">
<div class="stat-icon">
<i class="bi {{ $icon }}"></i>
</div>
<div>
<h6 class="stat-count">{{ $value }}</h6>
@php
// Logika untuk menentukan warna
$colorClass = 'text-success'; // Default hijau (bagus)
// Cek apakah ini metrik negatif (seperti Pesanan Ditolak)
if ($is_negative_metric ?? false) {
// Jika naik (> 0), berarti BURUK -> Merah
if ($grow > 0) {
$colorClass = 'text-danger';
}
} else {
// Untuk metrik biasa (Pendapatan, dll)
// Jika turun (< 0), berarti BURUK -> Merah
if ($grow < 0) {
$colorClass = 'text-danger';
}
}
@endphp
<span class="stat-percent {{ $colorClass }} fw-bold">
{{-- Ikon panah tetap menunjukkan arah matematis (naik/turun) --}}
<i class="bi {{ $grow >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
{{ abs($grow) }}%
</span>
<small class="stat-month">dari bulan lalu</small>
</div>
</div>
</div>
</div>
</div>