diff --git a/app/Http/Controllers/user/BookingFotoController.php b/app/Http/Controllers/user/BookingFotoController.php index 487b348..c4b6ad8 100644 --- a/app/Http/Controllers/user/BookingFotoController.php +++ b/app/Http/Controllers/user/BookingFotoController.php @@ -3,20 +3,248 @@ namespace App\Http\Controllers\User; use App\Http\Controllers\Controller; +use App\Models\Additional; +use App\Models\BookingFoto; +use App\Models\DetailAdditional; +use App\Models\PaketFoto; +use App\Models\Pelanggan; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class BookingFotoController extends Controller { public function index() { - return view('user/booking-foto'); + $foto = PaketFoto::latest()->get(); + + return view('user/booking-foto', compact('foto')); } - public function detail() + public function detail($id) { - return view('user/detail-foto'); + $foto = PaketFoto::findOrFail($id); + + // Jika add-ons disimpan di DB, ambil juga: + $additionals = Additional::all(); + // Logika Tanggal + $start = \Carbon\Carbon::now(); // Mulai hari ini + $end = \Carbon\Carbon::now()->addMonth(); // Maksimal 1 bulan ke depan + // Untuk navigasi panah + $prevMonth = $start->copy()->subMonth(); + $nextMonth = $start->copy()->addMonth(); + + $currentMonthLabel = $start->format('F Y'); + return view('user.detail-foto', compact('foto', 'additionals', 'start', 'end', 'currentMonthLabel', 'prevMonth', 'nextMonth')); } - public function formulir() + public function loadCalendar(Request $request) { - return view('user/pembayaran-foto'); + // Ambil bulan & tahun dari request AJAX, atau default sekarang + $month = $request->month ?? date('m'); + $year = $request->year ?? date('Y'); + + $start = \Carbon\Carbon::createFromDate($year, $month, 1); + + // Data Navigasi + $prevMonth = $start->copy()->subMonth(); + $nextMonth = $start->copy()->addMonth(); + $currentMonthLabel = $start->format('F Y'); + + // Return hanya potongan HTML (Partial), bukan halaman full + $html = view('user.components.calendar-grid', compact( + 'start', + 'prevMonth', + 'nextMonth', + 'currentMonthLabel' + ))->render(); + + // Return JSON agar JavaScript bisa membacanya sebagai data.html + return response()->json(['html' => $html]); + } + public function cekSlot(Request $request) + { + // Cari booking yang statusnya valid (bukan dibatalkan/ditolak) + // Sesuaikan status dengan logic bisnis kamu + $booked = BookingFoto::where('tgl_booking', $request->tanggal) + ->whereIn('status_booking', ['menunggu_verifikasi', 'diterima', 'selesai']) + ->get(['jam_mulai']); + + return response()->json($booked); + } + public function formulir(Request $request) + { + // 1. Ambil Data Paket + $foto = PaketFoto::findOrFail($request->id_paket); + + // 2. Hitung Total Add-ons (Jika ada) + $addonsDetails = []; + $totalAddon = 0; + + if ($request->has('addons')) { + foreach ($request->addons as $id => $qty) { + if ($qty > 0) { + $add = Additional::find($id); + $subtotal = $add->harga * $qty; + $totalAddon += $subtotal; + + $addonsDetails[] = [ + 'nama' => $add->nama, + 'qty' => $qty, + 'subtotal' => $subtotal, + 'id' => $id // Simpan ID untuk dikirim lagi nanti + ]; + } + } + } + + // 3. Hitung Grand Total + $grandTotal = $foto->harga + $totalAddon; + // 1. Cek apakah sudah ada deadline di session? Kalau belum, buat baru (2 jam dari sekarang) + if (!session()->has('payment_deadline')) { + $deadline = now()->addHours(2); + session()->put('payment_deadline', $deadline); + } else { + $deadline = session('payment_deadline'); + } + + // 2. Hitung sisa waktu dalam detik + $sisaWaktu = now()->diffInSeconds($deadline, false); // false = biar bisa negatif kalau lewat + + // 3. Jika waktu habis (negatif), hapus session dan tendang user + if ($sisaWaktu <= 0) { + session()->forget(['payment_deadline', 'addons']); // Bersihkan session + return redirect()->route('booking.foto')->with('error', 'Waktu pembayaran telah habis. Silakan ulang pemesanan.'); + } + // 4. Kirim semua data ke View Pembayaran untuk ditampilkan + return view('user.pembayaran-foto', compact( + 'foto', + 'request', // Kirim request agar tgl & jam bisa diakses di blade + 'addonsDetails', + 'grandTotal', + 'sisaWaktu' + )); + } + public function cancelBooking() + { + session()->forget(['payment_deadline', 'addons']); // Hapus session timer & data + return redirect()->route('booking.foto'); // Kembali ke katalog utama + } + public function store(Request $request) + { + + // 1. Validasi Input + $request->validate([ + 'id_paket' => 'required|exists:paket_fotos,id_paket', + 'tgl_booking' => 'required|date|after_or_equal:today', + 'jam_mulai' => 'required', + 'nama' => 'required|string|max:255', + 'no_wa' => 'required|numeric', + 'bukti_bayar' => 'required|image|mimes:jpeg,png,jpg|max:2048', + ]); + + DB::beginTransaction(); // Mulai Transaksi Database + + try { + // 2. Ambil Data Paket & Hitung Waktu + $paket = PaketFoto::findOrFail($request->id_paket); + + // Asumsi durasi default 20 menit (atau ambil dari database jika ada kolom durasi) + $durasiMenit = $paket->durasi; + $jamMulai = \Carbon\Carbon::createFromFormat('H:i', $request->jam_mulai); + $jamSelesai = $jamMulai->copy()->addMinutes($durasiMenit); + + // 3. Cek Slot Sekali Lagi (Mencegah Race Condition) + $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.'); + } + + // 4. Simpan/Update Data Pelanggan + $pelanggan = Pelanggan::firstOrCreate( + ['no_wa' => $request->no_wa], + ['nama' => $request->nama] + ); + + // 5. Upload Bukti Bayar + $pathBukti = null; + if ($request->hasFile('bukti_bayar')) { + $file = $request->file('bukti_bayar'); + $namaFile = 'bukti_' . time() . '_' . Str::random(5) . '.' . $file->getClientOriginalExtension(); + $file->move(public_path('img/payment'), $namaFile); + $pathBukti = 'img/payment/' . $namaFile; + } + + // 6. Hitung Grand Total (Paket + Additional) + // Kita hitung ulang di server agar aman dari manipulasi inspect element + $grandTotal = $paket->harga; + $listAdditional = []; + + if ($request->has('addons')) { + foreach ($request->addons as $idAddon => $qty) { + if ($qty > 0) { + $add = \App\Models\Additional::find($idAddon); + if ($add) { + $subtotal = $add->harga * $qty; + $grandTotal += $subtotal; + + $listAdditional[] = [ + 'id_additional' => $idAddon, + 'qty' => $qty, + 'subtotal' => $subtotal + ]; + } + } + } + } + + // 7. Simpan Booking Utama + $booking = BookingFoto::create([ + 'no_invoice' => 'INV-FOTO-' . strtoupper(Str::random(6)), + 'id_pelanggan' => $pelanggan->id_pelanggan, + 'id_paket' => $paket->id_paket, + 'tgl_booking' => $request->tgl_booking, + 'jam_mulai' => $request->jam_mulai, // Format "09:00" + 'jam_selesai' => $jamSelesai->format('H:i'), + 'total_bayar' => $grandTotal, + 'bukti_bayar' => $pathBukti, + 'status_booking' => 'menunggu_verifikasi' + ]); + + // 8. Simpan Detail Additional (Jika ada) + foreach ($listAdditional as $item) { + DetailAdditional::create([ + 'id_booking' => $booking->id_booking, + 'id_additional' => $item['id_additional'], + 'qty' => $item['qty'], + 'subtotal' => $item['subtotal'] + ]); + } + + DB::commit(); + // 9. Redirect ke WhatsApp atau Halaman Sukses + $pesan = "Halo Admin Flo.do! Saya pesan foto paket *{$paket->nama}*.\n" . + "Tanggal: {$request->tgl_booking}\n" . + "Jam: {$request->jam_mulai}\n" . + "Total: Rp " . number_format($grandTotal, 0, ',', '.') . "\n" . + "Mohon diverifikasi ya."; + + $urlWA = "https://wa.me/6289673668516?text=" . urlencode($pesan); + session()->forget('payment_deadline'); + + return redirect()->route('booking.foto')->with([ + 'success' => 'Pesanan Berhasil Dibuat!', + 'waUrl' => $urlWA + ]); + } catch (\Exception $e) { + DB::rollBack(); + // Teks Debugging Lengkap + $errorMsg = "Error: " . $e->getMessage() . " | Baris: " . $e->getLine() . " | File: " . basename($e->getFile()); + + // Kirim pesan error lengkap ke layar + return back()->with('error', $errorMsg)->withInput(); + } } } diff --git a/database/migrations/2025_12_26_150621_create_booking_fotos_table.php b/database/migrations/2025_12_26_150621_create_booking_fotos_table.php index 17d9b56..fa6ab07 100644 --- a/database/migrations/2025_12_26_150621_create_booking_fotos_table.php +++ b/database/migrations/2025_12_26_150621_create_booking_fotos_table.php @@ -26,7 +26,7 @@ public function up(): void $table->string('bukti_bayar')->nullable(); // Enum Status Final - $table->enum('status_booking', ['menunggu_verifikasi', 'diproses', 'selesai', 'dibatalkan'])->default('menunggu_verifikasi'); + $table->enum('status_booking', ['menunggu_verifikasi', 'diterima', 'ditolak', 'selesai', 'dibatalkan'])->default('menunggu_verifikasi'); $table->timestamps(); }); diff --git a/database/seeders/PaketFotoSeeder.php b/database/seeders/PaketFotoSeeder.php index 6852029..8270d5c 100644 --- a/database/seeders/PaketFotoSeeder.php +++ b/database/seeders/PaketFotoSeeder.php @@ -11,25 +11,60 @@ public function run(): void { $data = [ [ - 'nama' => 'Wisuda Basic', - 'harga' => 250000, - 'deskripsi' => 'Foto wisuda outdoor 1 jam, 10 edit file, all file mentah.', - 'foto' => 'img/foto/foto1.jpeg', - 'durasi' => '60', + 'nama' => 'Single', + 'harga' => 20000, + 'deskripsi' => 'Untuk 1 orang, 10 menit sesi foto sepuasnya, 5 menit sesi pilih foto (jika ada yang di print).', + 'foto' => 'img/foto/single.jpeg', + 'durasi' => '10', ], [ - 'nama' => 'Couple Studio Session', - 'harga' => 350000, - 'deskripsi' => 'Foto studio couple 45 menit, 2 cetak 10R, 5 edit file.', - 'foto' => 'img/foto/foto2.jpeg', - 'durasi' => '45', + 'nama' => 'Couple', + 'harga' => 45000, + 'deskripsi' => 'Untuk 2 orang, 15 menit sesi foto sepuasnya, 5 menit sesi pilih foto, 1 lembar print out foto ukutan 4R.', + 'foto' => 'img/foto/grup.jpeg', + 'durasi' => '15', ], [ - 'nama' => 'Group Photoshoot (Max 10 Orang)', - 'harga' => 500000, - 'deskripsi' => 'Foto grup, cocok untuk angkatan atau keluarga besar. Durasi 2 jam.', - 'foto' => 'img/foto/foto3.jpeg', - 'durasi' => '120', + 'nama' => 'Group', + 'harga' => 80000, + 'deskripsi' => 'Untuk 3-5 orang, 15 menit sesi foto sepuasnya, 5 menit sesi pilih foto, 3 lembar print out foto ukutan 4R.', + 'foto' => 'img/foto/pas-foto.jpg', + 'durasi' => '15', + ], + [ + 'nama' => 'Pas Foto Paket 1', + 'harga' => 25000, + 'deskripsi' => 'Sesi pas foto untuk 1 orang dengan 8x shoot fotografer. Termasuk 1 file foto edit dan bebas request warna background. Paket cetak: ukuran 2x3 (12 lembar).', + 'foto' => 'img/foto/pas-foto.jpg', + 'durasi' => '0', + ], + [ + 'nama' => 'Pas Foto Paket 2', + 'harga' => 25000, + 'deskripsi' => 'Sesi pas foto untuk 1 orang dengan 8x shoot fotografer. Termasuk 1 file foto edit dan bebas request warna background. Paket cetak: ukuran 3x4 (8 lembar).', + 'foto' => 'img/foto/pas-foto.jpg', + 'durasi' => '0', + ], + [ + 'nama' => 'Pas Foto Paket 3', + 'harga' => 25000, + 'deskripsi' => 'Sesi pas foto untuk 1 orang dengan 8x shoot fotografer. Termasuk 1 file foto edit dan bebas request warna background. Paket cetak: ukuran 4x6 (4 lembar).', + 'foto' => 'img/foto/pas-foto.jpg', + 'durasi' => '0', + ], + [ + 'nama' => 'Pas Foto Paket 4', + 'harga' => 25000, + 'deskripsi' => 'Sesi pas foto untuk 1 orang dengan 8x shoot fotografer. Termasuk 1 file foto edit dan bebas request warna background. Paket cetak campur: ukuran 4x6 (2 lembar), 3x4 (3 lembar), dan 2x3 (4 lembar).', + 'foto' => 'img/foto/pas-foto.jpg', + 'durasi' => '0', + ], + [ + 'nama' => 'Background Biru', + 'harga' => 95000, + 'deskripsi' => 'Paket foto untuk 2 orang. Termasuk: Max 10x shoot fotografer (formal), 10 menit self-photo (bebas), dan semua file dikirim via Google Drive. Cetak 2 Foto (4R) terdiri dari: (2x3) 8 lembar, (3x4) 6 lembar, dan (4x6) 4 lembar.', + 'foto' => 'img/foto/latar-biru.jpeg', + 'durasi' => '20', ], ]; diff --git a/resources/views/user/booking-foto.blade.php b/resources/views/user/booking-foto.blade.php index b72f6e0..b9231cc 100644 --- a/resources/views/user/booking-foto.blade.php +++ b/resources/views/user/booking-foto.blade.php @@ -16,27 +16,67 @@