Refactor booking and payment flow for photo packages
- Updated booking-foto.blade.php to use forelse for displaying photo packages and added SweetAlert notifications for successful and error sessions. - Created a new calendar-grid component for better date selection in detail-foto.blade.php. - Enhanced detail-foto.blade.php to include a booking form with additional options and a calendar for selecting dates and times. - Improved pembayaran-foto.blade.php to handle errors, include hidden fields for previous selections, and add file upload preview functionality. - Updated web.php routes to better organize user and admin routes, including AJAX endpoints for calendar loading and slot checking.
This commit is contained in:
parent
24780837c2
commit
1f94e59459
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -16,27 +16,67 @@
|
|||
<div class="row">
|
||||
<div class="col-12" style="max-width: 850px; margin: 0 auto;">
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4 justify-content-center">
|
||||
|
||||
@for ($i = 1; $i <= 8; $i++)
|
||||
@forelse ($foto as $f)
|
||||
<div class="col">
|
||||
<div class="bookingfoto-card h-100">
|
||||
<div class="bookingfoto-card position-relative">
|
||||
<div class="img-wrapper mb-3">
|
||||
<img src="{{ asset('img/hero-foto.jpg') }}" class="img-fluid rounded-4"
|
||||
alt="Paket Foto">
|
||||
<img src="{{ asset($f->foto) }}" class="img-fluid rounded-4"
|
||||
alt="{{ $f->nama }}">
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h6 class="bookingfoto-product-title">Paket Self Photo {{ $i }}</h6>
|
||||
<p class="bookingfoto-product-price">Rp 85.000</p>
|
||||
<h6 class="bookingfoto-product-title">{{ $f->nama }}</h6>
|
||||
<p class="bookingfoto-product-price">Rp {{ number_format($f->harga, 0, ',', '.') }}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ route('detail.foto') }}" class="stretched-link"></a>
|
||||
<a href="{{ route('detail.foto', $f->id_paket) }}" class="stretched-link"></a>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
|
||||
@empty
|
||||
<div class="col-12 text-center py-5">
|
||||
<p>Paket foto tidak ditemukan</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{{-- Cek session waUrl agar sama dengan pola buket --}}
|
||||
@if (session('waUrl'))
|
||||
{{-- Panggil library SweetAlert2 secara lokal di sini untuk memastikan ia ada --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
// Gunakan pola yang sama dengan buket yang sudah terbukti jalan
|
||||
Swal.fire({
|
||||
title: 'Pesanan Berhasil!',
|
||||
text: "{{ session('success') }}",
|
||||
icon: 'success',
|
||||
confirmButtonText: 'Konfirmasi WhatsApp',
|
||||
confirmButtonColor: '#20c997',
|
||||
allowOutsideClick: false
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Buka WhatsApp di tab baru
|
||||
window.open("{{ session('waUrl') }}", '_blank');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
|
||||
{{-- Pisahkan handler error --}}
|
||||
@if (session('error'))
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
Swal.fire({
|
||||
title: 'Gagal!',
|
||||
text: "{{ session('error') }}",
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545'
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<div class="d-flex justify-content-between align-items-center mb-3 calendar-nav">
|
||||
<button type="button" class="btn btn-sm btn-light rounded-circle cal-nav-btn"
|
||||
onclick="changeMonth({{ $prevMonth->month }}, {{ $prevMonth->year }})">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<span class="cal-month-label">{{ $currentMonthLabel }}</span>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-light rounded-circle cal-nav-btn"
|
||||
onclick="changeMonth({{ $nextMonth->month }}, {{ $nextMonth->year }})">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="detailfoto-date-grid">
|
||||
<div class="day-name">Su</div>
|
||||
<div class="day-name">Mo</div>
|
||||
<div class="day-name">Tu</div>
|
||||
<div class="day-name">We</div>
|
||||
<div class="day-name">Th</div>
|
||||
<div class="day-name">Fr</div>
|
||||
<div class="day-name">Sa</div>
|
||||
|
||||
{{-- Offset Spasi --}}
|
||||
@for ($i = 0; $i < $start->copy()->startOfMonth()->dayOfWeek; $i++)
|
||||
<div class="date-item empty"></div>
|
||||
@endfor
|
||||
|
||||
{{-- Loop Tanggal --}}
|
||||
@php $daysInMonth = $start->copy()->daysInMonth; @endphp
|
||||
@for ($day = 1; $day <= $daysInMonth; $day++)
|
||||
@php
|
||||
$dateFull = $start->copy()->day($day);
|
||||
// Logic Disabled: Lewat hari ini ATAU lebih dari 30 hari ke depan
|
||||
$isPast = $dateFull->isPast() && !$dateFull->isToday();
|
||||
$isTooFar = $dateFull->diffInDays(\Carbon\Carbon::now()) > 30;
|
||||
$isDisabled = $isPast || $isTooFar;
|
||||
@endphp
|
||||
|
||||
<div class="date-item {{ $isDisabled ? 'disabled' : '' }}" data-date="{{ $dateFull->format('Y-m-d') }}">
|
||||
{{ $day }}
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
|
|
@ -12,204 +12,338 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-6 mb-5" id="left-column">
|
||||
{{-- Seluruh Konten dalam Form agar data Add-ons & Slot terkirim --}}
|
||||
<form id="form-booking" action="{{ route('formulir.foto') }}" method="GET">
|
||||
@csrf
|
||||
{{-- Kotak simpan data sementara di dalam form --}}
|
||||
<input type="hidden" name="tgl_booking" id="input_tgl_booking">
|
||||
<input type="hidden" name="jam_awal" id="input_jam_awal">
|
||||
<input type="hidden" name="jam_selesai" id="input_jam_selesai">
|
||||
<div id="data-paket" data-durasi="15"></div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-5" id="left-column">
|
||||
<input type="hidden" name="id_paket" value="{{ $foto->id_paket }}">
|
||||
|
||||
<div class="row gx-2">
|
||||
<div class="col-auto">
|
||||
<div class="detailfoto-card p-2 d-flex align-items-center h-100">
|
||||
<img src="{{ asset('img/hero-foto.jpg') }}" alt="Paket Single"
|
||||
class="detailfoto-thumb rounded-4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="detailfoto-card p-3 d-flex align-items-center h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="detailfoto-product-title">Single</h3>
|
||||
<h4 class="detailfoto-product-price">Rp 20.000</h4>
|
||||
<p class="detailfoto-product-desc mb-0"> Untuk 1 menit • 10 menit sesi foto sepuasnya •
|
||||
5 menit sesi pilih foto (jika ada yang diprint)
|
||||
</p>
|
||||
<div class="row gx-3">
|
||||
<div class="col-auto">
|
||||
<div class="detailfoto-card p-2">
|
||||
<img src="{{ asset($foto->foto) }}" alt="{{ $foto->nama }}"
|
||||
class="detailfoto-thumb rounded-4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="detailfoto-addons-title">Additional</h5>
|
||||
<div class="detailfoto-addons-wrapper">
|
||||
<div class="detailfoto-addon-item">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="detailfoto-addon-name">Tambah orang untuk paket grup</span>
|
||||
<span class="detailfoto-addon-price">Rp 15.000</span>
|
||||
</div>
|
||||
<div class="detailfoto-counter">
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, -1)">-</button>
|
||||
<input type="text" value="0" readonly>
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detailfoto-addon-item">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="detailfoto-addon-name">Kostum boneka (onesize)/orang</span>
|
||||
<span class="detailfoto-addon-price">Rp 10.000</span>
|
||||
</div>
|
||||
<div class="detailfoto-counter">
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, -1)">-</button>
|
||||
<input type="text" value="0" readonly>
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detailfoto-addon-item">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="detailfoto-addon-name">Tambah print out foto 4R/lembar</span>
|
||||
<span class="detailfoto-addon-price">Rp 7.000</span>
|
||||
</div>
|
||||
<div class="detailfoto-counter">
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, -1)">-</button>
|
||||
<input type="text" value="0" readonly>
|
||||
<button type="button" class="btn-counter" onclick="updateCounter(this, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center" id="btn-booking-wrapper">
|
||||
<button type="button" class="btn btn-detailfoto-primary" onclick="showCalendar()">
|
||||
Booking Slot Foto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 d-none" id="calendar-column">
|
||||
<div class="detailfoto-calendar-box">
|
||||
<div class="mb-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="timer-alert-text">Slot Anda akan dibatalkan otomatis dalam</span>
|
||||
<span class="timer-badge">09:58</span>
|
||||
</div>
|
||||
|
||||
<h4 class="schedule-title">Pilih Tanggal dan Waktu Pemotretan</h4>
|
||||
|
||||
<p class="schedule-desc">
|
||||
Slot abu-abu berarti jadwal sudah penuh. Silakan pilih waktu lain yang tersedia.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-md-9 border-end pe-md-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 calendar-nav">
|
||||
|
||||
<button type="button" class="btn btn-sm btn-light rounded-circle cal-nav-btn prev">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<span class="cal-month-label">June 2025</span>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-light rounded-circle cal-nav-btn next">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="detailfoto-date-grid">
|
||||
<div class="day-name">Su</div>
|
||||
<div class="day-name">Mo</div>
|
||||
<div class="day-name">Tu</div>
|
||||
<div class="day-name">We</div>
|
||||
<div class="day-name">Th</div>
|
||||
<div class="day-name">Fr</div>
|
||||
<div class="day-name">Sa</div>
|
||||
|
||||
<div class="date-item disabled">1</div>
|
||||
<div class="date-item disabled">2</div>
|
||||
<div class="date-item disabled">3</div>
|
||||
<div class="date-item disabled">4</div>
|
||||
<div class="date-item disabled">5</div>
|
||||
<div class="date-item disabled">6</div>
|
||||
<div class="date-item disabled">7</div>
|
||||
<div class="date-item disabled">8</div>
|
||||
<div class="date-item disabled">9</div>
|
||||
<div class="date-item selected">10</div>
|
||||
<div class="date-item">11</div>
|
||||
<div class="date-item">12</div>
|
||||
<div class="date-item">13</div>
|
||||
<div class="date-item">14</div>
|
||||
<div class="date-item">15</div>
|
||||
<div class="date-item">16</div>
|
||||
<div class="date-item">17</div>
|
||||
<div class="date-item">18</div>
|
||||
<div class="date-item">19</div>
|
||||
<div class="date-item">20</div>
|
||||
<div class="date-item">21</div>
|
||||
<div class="date-item">22</div>
|
||||
<div class="date-item">23</div>
|
||||
<div class="date-item">24</div>
|
||||
<div class="date-item">25</div>
|
||||
<div class="date-item">26</div>
|
||||
<div class="date-item">27</div>
|
||||
<div class="date-item">28</div>
|
||||
<div class="date-item">29</div>
|
||||
<div class="date-item">30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 ps-md-3">
|
||||
<h6 class=" mb-3 small">Jam Tersedia</h6>
|
||||
|
||||
<div class="time-slot-container">
|
||||
<button class="btn-time">09:00 AM</button>
|
||||
<button class="btn-time">09:30 AM</button>
|
||||
<button class="btn-time active">10:00 AM</button>
|
||||
<button class="btn-time disabled">10:30 AM</button>
|
||||
<button class="btn-time">11:00 AM</button>
|
||||
<button class="btn-time">11:30 AM</button>
|
||||
<button class="btn-time">12:00 PM</button>
|
||||
<button class="btn-time">13:00 PM</button>
|
||||
<div class="col">
|
||||
<div class="detailfoto-card p-3 h-100">
|
||||
<h3 class="detailfoto-product-title">{{ $foto->nama }}</h3>
|
||||
<h4 class="detailfoto-product-price">Rp {{ number_format($foto->harga, 0, ',', '.') }}
|
||||
</h4>
|
||||
<p class="detailfoto-product-desc mb-0 text-justify">{{ $foto->deskripsi }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 mt-4 pt-4">
|
||||
<h5 class="detailfoto-addons-title mt-4">Additional</h5>
|
||||
<div class="detailfoto-addons-wrapper">
|
||||
@foreach ($additionals as $add)
|
||||
<div class="detailfoto-addon-item">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="detailfoto-addon-name">{{ $add->nama }}</span>
|
||||
<span class="detailfoto-addon-price">Rp
|
||||
{{ number_format($add->harga, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
<div class="detailfoto-counter">
|
||||
<button type="button" class="btn-counter"
|
||||
onclick="updateCounter(this, -1)">-</button>
|
||||
<input type="text" name="addons[{{ $add->id_additional }}]" value="0"
|
||||
readonly>
|
||||
<button type="button" class="btn-counter"
|
||||
onclick="updateCounter(this, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<button class="btn btn-action-cancel flex-fill" onclick="hideCalendar()">
|
||||
Batalkan
|
||||
<div class="mt-4 text-center" id="btn-booking-wrapper">
|
||||
<button type="button" class="btn btn-detailfoto-primary" onclick="showCalendar()">
|
||||
Booking Slot Foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('formulir.foto') }}" class="btn btn-action-submit flex-fill">
|
||||
Pesan Sekarang
|
||||
</a>
|
||||
<div class="col-lg-6 d-none" id="calendar-column">
|
||||
<div class="detailfoto-calendar-box">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="timer-alert-text">Slot Anda akan dibatalkan otomatis dalam</span>
|
||||
<span class="timer-badge" id="booking-timer">10:00</span>
|
||||
</div>
|
||||
<h4 class="schedule-title">Pilih Tanggal dan Waktu</h4>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-9 border-end pe-md-4" id="calendar-wrapper">
|
||||
@include('user.components.calendar-grid')
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-3 small">Jam Tersedia</h6>
|
||||
<div class="time-slot-container">
|
||||
@for ($hour = 9; $hour <= 20; $hour++)
|
||||
<button type="button" class="btn-time"
|
||||
data-time="{{ sprintf('%02d:00', $hour) }}">
|
||||
{{ sprintf('%02d:00', $hour) }} {{ $hour < 12 ? 'AM' : 'PM' }}
|
||||
</button>
|
||||
<button type="button" class="btn-time"
|
||||
data-time="{{ sprintf('%02d:30', $hour) }}">
|
||||
{{ sprintf('%02d:30', $hour) }} {{ $hour < 12 ? 'AM' : 'PM' }}
|
||||
</button>
|
||||
@endfor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 mt-4 pt-4">
|
||||
<button type="button" class="btn btn-action-cancel flex-fill"
|
||||
onclick="hideCalendar()">Batalkan</button>
|
||||
<button type="submit" class="btn btn-action-submit flex-fill">Pesan Sekarang</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function showCalendar() {
|
||||
// 1. Sembunyikan tombol booking awal
|
||||
document.getElementById('btn-booking-wrapper').style.display = 'none';
|
||||
// ============================================================
|
||||
// 1. VARIABLE GLOBAL & UTILITIES
|
||||
// ============================================================
|
||||
let countdown; // Variabel untuk menyimpan timer
|
||||
const durasiPaket = parseInt(document.getElementById('data-paket')?.dataset.durasi || 15);
|
||||
|
||||
// 2. Munculkan kolom kanan dengan animasi
|
||||
const calendarCol = document.getElementById('calendar-column');
|
||||
calendarCol.classList.remove('d-none');
|
||||
calendarCol.classList.add('fade-in-right'); // Custom CSS animation
|
||||
}
|
||||
|
||||
// Fungsi Counter +/-
|
||||
// Fungsi Update Counter (+/-) untuk Additional
|
||||
function updateCounter(btn, change) {
|
||||
const input = btn.parentElement.querySelector('input');
|
||||
let newValue = parseInt(input.value) + change;
|
||||
if (newValue < 0) newValue = 0;
|
||||
let currentValue = parseInt(input.value) || 0;
|
||||
let newValue = currentValue + change;
|
||||
|
||||
if (newValue < 0) newValue = 0; // Cegah minus
|
||||
input.value = newValue;
|
||||
}
|
||||
|
||||
// Fungsi Tampilkan Kalender (Step 1 -> Step 2)
|
||||
function showCalendar() {
|
||||
document.getElementById('btn-booking-wrapper').style.display = 'none';
|
||||
|
||||
// DISABLE KOLOM KIRI
|
||||
const leftColumn = document.getElementById('left-column');
|
||||
if (leftColumn) leftColumn.classList.add('disabled-section');
|
||||
|
||||
const calendarCol = document.getElementById('calendar-column');
|
||||
if (calendarCol) {
|
||||
calendarCol.classList.remove('d-none');
|
||||
calendarCol.classList.add('fade-in-right');
|
||||
calendarCol.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
startTimer(10 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi Sembunyikan Kalender (Tombol Batalkan)
|
||||
function hideCalendar() {
|
||||
if (countdown) clearInterval(countdown);
|
||||
|
||||
document.getElementById('calendar-column').classList.add('d-none');
|
||||
document.getElementById('btn-booking-wrapper').style.display = 'block';
|
||||
|
||||
// ENABLE KEMBALI KOLOM KIRI
|
||||
const leftColumn = document.getElementById('left-column');
|
||||
if (leftColumn) leftColumn.classList.remove('disabled-section');
|
||||
|
||||
// Scroll kembali ke atas (opsional, agar user sadar kolom kiri aktif lagi)
|
||||
leftColumn.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// Reset pilihan tanggal & jam di input hidden
|
||||
document.getElementById('input_tgl_booking').value = "";
|
||||
document.getElementById('input_jam_awal').value = "";
|
||||
}
|
||||
|
||||
// Fungsi Timer Mundur
|
||||
function startTimer(duration) {
|
||||
let timer = duration,
|
||||
minutes, seconds;
|
||||
const display = document.querySelector('#booking-timer');
|
||||
|
||||
// Reset timer lama jika ada
|
||||
if (countdown) clearInterval(countdown);
|
||||
|
||||
countdown = setInterval(function() {
|
||||
minutes = parseInt(timer / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
if (display) display.textContent = minutes + ":" + seconds;
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(countdown);
|
||||
Swal.fire('Waktu Habis!', 'Sesi booking Anda telah berakhir. Silakan pilih ulang.', 'warning')
|
||||
.then(() => location.reload());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Helper: Hitung Jam Selesai
|
||||
function calculateEndTime(startTime, duration) {
|
||||
let [hours, minutes] = startTime.split(':').map(Number);
|
||||
minutes += duration;
|
||||
|
||||
if (minutes >= 60) {
|
||||
hours += Math.floor(minutes / 60);
|
||||
minutes = minutes % 60;
|
||||
}
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 2. LOGIC AJAX & INTERAKSI KALENDER
|
||||
// ============================================================
|
||||
|
||||
// A. Fungsi Ganti Bulan (Tanpa Refresh)
|
||||
function changeMonth(month, year) {
|
||||
const wrapper = document.getElementById('calendar-grid-wrapper');
|
||||
|
||||
// Efek loading
|
||||
wrapper.style.opacity = '0.5';
|
||||
|
||||
// Panggil Controller via AJAX
|
||||
fetch(`/load-calendar?month=${month}&year=${year}`)
|
||||
.then(response => response.json()) // Pastikan controller return JSON { html: "..." }
|
||||
.then(data => {
|
||||
// Ganti isi wrapper dengan HTML baru dari server
|
||||
wrapper.innerHTML = data.html;
|
||||
wrapper.style.opacity = '1';
|
||||
|
||||
// PENTING: Pasang ulang listener karena elemen HTML-nya baru
|
||||
reinitDateListeners();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Gagal load kalender:", err);
|
||||
wrapper.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
|
||||
// B. Pasang Event Listener ke Tanggal (Re-usable)
|
||||
function reinitDateListeners() {
|
||||
const dateItems = document.querySelectorAll('.date-item:not(.disabled):not(.empty)');
|
||||
|
||||
dateItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
// UI: Reset warna selected lama
|
||||
document.querySelectorAll('.date-item').forEach(el => el.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
|
||||
// Logic: Simpan tanggal ke input hidden
|
||||
const tgl = this.dataset.date;
|
||||
const tglInput = document.getElementById('input_tgl_booking');
|
||||
if (tglInput) tglInput.value = tgl;
|
||||
|
||||
// Logic: Cek Slot ke Database
|
||||
checkSlotAvailability(tgl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// C. Cek Slot Penuh via AJAX
|
||||
function checkSlotAvailability(tgl) {
|
||||
// 1. Reset semua tombol jam jadi aktif dulu
|
||||
const allTimeBtns = document.querySelectorAll('.btn-time');
|
||||
allTimeBtns.forEach(btn => {
|
||||
btn.classList.remove('disabled', 'full', 'active');
|
||||
btn.disabled = false;
|
||||
btn.title = "";
|
||||
});
|
||||
|
||||
// 2. Kosongkan input jam (karena user ganti tanggal)
|
||||
document.getElementById('input_jam_awal').value = "";
|
||||
|
||||
// 3. Panggil Server
|
||||
fetch(`/cek-slot-foto?tanggal=${tgl}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(booking => {
|
||||
// Ambil jam depan saja (09:00:00 -> 09:00)
|
||||
const jamPenuh = booking.jam_mulai.substring(0, 5);
|
||||
|
||||
// Cari tombol yg punya data-time sama
|
||||
const btnPenuh = document.querySelector(`.btn-time[data-time="${jamPenuh}"]`);
|
||||
|
||||
if (btnPenuh) {
|
||||
btnPenuh.classList.add('disabled', 'full'); // Tambah class styling
|
||||
btnPenuh.disabled = true; // Matikan klik
|
||||
btnPenuh.title = "Slot Sudah Terisi";
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => console.error("Gagal cek slot:", err));
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 3. INISIALISASI SAAT HALAMAN DIMUAT (DOM READY)
|
||||
// ============================================================
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// 1. Jalankan Listener Tanggal (Untuk kalender awal)
|
||||
reinitDateListeners();
|
||||
|
||||
// 2. Pasang Listener Tombol Jam (Cukup sekali)
|
||||
const timeButtons = document.querySelectorAll('.btn-time');
|
||||
timeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Cek jika tombol disabled (safety)
|
||||
if (this.disabled || this.classList.contains('disabled')) return;
|
||||
|
||||
// UI: Reset active
|
||||
timeButtons.forEach(el => el.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Logic: Simpan Jam
|
||||
const startTime = this.dataset.time;
|
||||
document.getElementById('input_jam_awal').value = startTime;
|
||||
|
||||
// Logic: Hitung Jam Selesai
|
||||
const endTime = calculateEndTime(startTime, durasiPaket);
|
||||
document.getElementById('input_jam_selesai').value = endTime;
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Validasi Submit Form
|
||||
const bookingForm = document.getElementById('form-booking');
|
||||
if (bookingForm) {
|
||||
bookingForm.addEventListener('submit', function(e) {
|
||||
const tgl = document.getElementById('input_tgl_booking').value;
|
||||
const jam = document.getElementById('input_jam_awal').value;
|
||||
|
||||
if (!tgl || !jam) {
|
||||
e.preventDefault(); // Stop kirim
|
||||
Swal.fire({
|
||||
title: 'Jadwal Belum Dipilih!',
|
||||
text: 'Silakan pilih tanggal dan jam pemotretan terlebih dahulu.',
|
||||
icon: 'warning',
|
||||
confirmButtonColor: '#20c997'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -11,9 +11,28 @@
|
|||
<h2 class="formulirfoto-page-title">Formulir Pemesanan</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="#" method="POST" enctype="multipart/form-data">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
<form action="{{ route('transaksi.foto.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
{{-- DATA DARI HALAMAN SEBELUMNYA (WAJIB ADA) --}}
|
||||
<input type="hidden" name="id_paket" value="{{ $foto->id_paket }}">
|
||||
<input type="hidden" name="tgl_booking" value="{{ $request->tgl_booking }}">
|
||||
<input type="hidden" name="jam_mulai" value="{{ $request->jam_mulai ?? $request->jam_awal }}">
|
||||
|
||||
{{-- Loop untuk mengirim ulang data Add-ons yang dipilih --}}
|
||||
@if (!empty($addonsDetails))
|
||||
@foreach ($addonsDetails as $add)
|
||||
<input type="hidden" name="addons[{{ $add['id'] }}]" value="{{ $add['qty'] }}">
|
||||
@endforeach
|
||||
@endif
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-lg-6 mb-5 mb-lg-0">
|
||||
|
|
@ -22,14 +41,14 @@
|
|||
|
||||
<div>
|
||||
<label class="form-label small">Nama Lengkap</label>
|
||||
<input type="text" class="form-control formulirfoto-input"
|
||||
placeholder="Masukkan Nama Lengkap">
|
||||
<input type="text" name="nama" class="form-control formulirfoto-input"
|
||||
placeholder="Masukkan Nama Lengkap" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label small">Nomor WhatsApp</label>
|
||||
<input type="number" class="form-control formulirfoto-input"
|
||||
placeholder="Masukkan Nomor WhatsApp">
|
||||
<input type="number" name="no_wa" class="form-control formulirfoto-input"
|
||||
placeholder="Masukkan Nomor WhatsApp" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,30 +64,38 @@
|
|||
<div class="summary-item">
|
||||
<span class="summary-label">Jadwal Booking:</span>
|
||||
<div class="summary-row">
|
||||
<span class="summary-subtext">Thursday, June 10, 2025</span>
|
||||
<span class="summary-value">10:00 PM</span>
|
||||
<span
|
||||
class="summary-subtext">{{ \Carbon\Carbon::parse($request->tgl_booking)->translatedFormat('l, d F Y') }}</span>
|
||||
<span class="summary-value">{{ $request->jam_mulai }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Paket Foto:</span>
|
||||
<div class="summary-row">
|
||||
<span class="summary-subtext">Single</span>
|
||||
<span class="summary-value">Rp 20.000</span>
|
||||
<span class="summary-subtext">{{ $foto->nama }}</span>
|
||||
<span class="summary-value">Rp {{ number_format($foto->harga, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Additional:</span>
|
||||
<div class="summary-row">
|
||||
<div class="row-left">
|
||||
<span class="summary-subtext">Kostum boneka (onesize)</span>
|
||||
<span class="summary-qty">x1</span>
|
||||
@forelse($addonsDetails as $add)
|
||||
<div class="summary-row">
|
||||
<div class="row-left">
|
||||
<span class="summary-subtext">{{ $add['nama'] }}</span>
|
||||
<span class="summary-qty">x{{ $add['qty'] }}</span>
|
||||
</div>
|
||||
<span class="summary-value">Rp
|
||||
{{ number_format($add['subtotal'], 0, ',', '.') }}</span>
|
||||
</div>
|
||||
<span class="summary-value">Rp 20.000</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="summary-row">
|
||||
<span class="summary-subtext text-muted">- Tidak ada tambahan -</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<span class="text-muted">Total Pembayaran</span>
|
||||
<h5 class=" mb-0">Rp 150.000</h5>
|
||||
<h5 class=" mb-0">Rp {{ number_format($grandTotal, 0, ',', '.') }}</h5>
|
||||
</div>
|
||||
<p class="small mb-2">Transfer ke Rekening Berikut:</p>
|
||||
<div class="row g-2 mb-4">
|
||||
|
|
@ -110,8 +137,8 @@ class="btn btn-sm btn-outline-secondary py-1 px-3 x-small btn-copy"
|
|||
<div class="formulirbuket-upload-area mb-2 text-center position-relative">
|
||||
<input type="file"
|
||||
class="position-absolute w-100 h-100 opacity-0 start-0 top-0 cursor-pointer"
|
||||
id="fileUpload">
|
||||
<div class="py-4">
|
||||
name="bukti_bayar" accept="image/*" id="fileUpload">
|
||||
<div class="py-4" id="uploadPlaceholder">
|
||||
<i class="bi bi-file-earmark-arrow-up fs-3 text-secondary"></i>
|
||||
<p class="mb-0 small text-muted">Upload Bukti Pembayaran</p>
|
||||
<p class="mb-0 x-small text-muted">Max. 2 MB</p>
|
||||
|
|
@ -123,9 +150,12 @@ class="position-absolute w-100 h-100 opacity-0 start-0 top-0 cursor-pointer"
|
|||
</p>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
<a href="{{ route('detail.foto') }}"
|
||||
<a href="{{ route('booking.cancel') }}"
|
||||
class="btn formulirfoto-btn-cancel flex-fill">Batalkan</a>
|
||||
<button type="submit" class="btn formulirfoto-btn-submit flex-fill">Kirim Pesanan</button>
|
||||
{{-- <a href="{{ route('detail.foto', $foto->id_paket) }}"
|
||||
class="btn formulirfoto-btn-cancel flex-fill">Batalkan</a> --}}
|
||||
<button type="submit" class="btn formulirfoto-btn-submit flex-fill">Kirim
|
||||
Pesanan</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -137,41 +167,74 @@ class="btn formulirfoto-btn-cancel flex-fill">Batalkan</a>
|
|||
</section>
|
||||
|
||||
<script>
|
||||
// Fitur Copy
|
||||
document.querySelectorAll('.btn-copy').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
navigator.clipboard.writeText(this.getAttribute('data-clipboard-text'));
|
||||
let originalText = this.innerText;
|
||||
this.innerText = 'Disalin!';
|
||||
setTimeout(() => {
|
||||
this.innerText = originalText;
|
||||
}, 1500);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// 1. FITUR COPY NO REK
|
||||
document.querySelectorAll('.btn-copy').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
navigator.clipboard.writeText(this.getAttribute('data-clipboard-text'));
|
||||
let originalText = this.innerText;
|
||||
this.innerText = 'Disalin!';
|
||||
setTimeout(() => {
|
||||
this.innerText = originalText;
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
// 2. FITUR PREVIEW UPLOAD
|
||||
const fileInput = document.getElementById('fileUpload');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
const placeholder = document.getElementById('uploadPlaceholder');
|
||||
if (file) {
|
||||
placeholder.innerHTML = `
|
||||
<i class="bi bi-check-circle-fill fs-3 text-success"></i>
|
||||
<p class="mb-0 small text-success fw-bold">${file.name}</p>
|
||||
<p class="mb-0 x-small text-muted">Klik lagi untuk ganti file</p>
|
||||
`;
|
||||
// Optional: Kasih border hijau biar makin jelas
|
||||
placeholder.parentElement.style.borderColor = "#198754";
|
||||
placeholder.parentElement.style.backgroundColor = "#e8f5e9";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. FITUR TIMER MUNDUR
|
||||
let sisaDetik = {{ $sisaWaktu }};
|
||||
let display = document.querySelector('#countdown-timer');
|
||||
|
||||
function startTimer(duration) {
|
||||
let timer = duration,
|
||||
hours, minutes, seconds;
|
||||
|
||||
let interval = setInterval(function() {
|
||||
hours = parseInt(timer / 3600, 10);
|
||||
minutes = parseInt((timer % 3600) / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
hours = hours < 10 ? "0" + hours : hours;
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
if (display) display.textContent = hours + ":" + minutes + ":" + seconds;
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(interval);
|
||||
alert("Waktu pembayaran habis!");
|
||||
window.location.href =
|
||||
"{{ route('booking.cancel') }}"; // Redirect ke cancel agar session bersih
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Jalankan timer
|
||||
if (sisaDetik > 0) {
|
||||
startTimer(sisaDetik);
|
||||
} else {
|
||||
// Jaga-jaga kalau sisaDetik 0 pas load (redirect langsung)
|
||||
window.location.href = "{{ route('booking.cancel') }}";
|
||||
}
|
||||
});
|
||||
|
||||
// Fitur Timer Mundur (Simulasi)
|
||||
function startTimer(duration, display) {
|
||||
var timer = duration,
|
||||
minutes, seconds;
|
||||
setInterval(function() {
|
||||
minutes = parseInt(timer / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
display.textContent = "00:" + minutes + ":" + seconds;
|
||||
|
||||
if (--timer < 0) {
|
||||
timer = duration;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
var fiftyEightMinutes = 60 * 58 + 58, // 58 menit 58 detik
|
||||
display = document.querySelector('#countdown-timer');
|
||||
startTimer(fiftyEightMinutes, display);
|
||||
};
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
110
routes/web.php
110
routes/web.php
|
|
@ -1,45 +1,95 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Admin as Admin; // Import namespace Admin
|
||||
use App\Http\Controllers\Admin as Admin;
|
||||
use App\Http\Controllers\User as User;
|
||||
use App\Http\Controllers\AuthController; // Asumsi controller login dipisah atau di Admin
|
||||
use App\Http\Controllers\User\TestBookingController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 1. PUBLIK / GUEST (Pelanggan)
|
||||
|--------------------------------------------------------------------------
|
||||
| Bebas akses, tidak perlu login.
|
||||
*/
|
||||
|
||||
Route::group([], function () {
|
||||
Route::get('/', [App\Http\Controllers\User\BerandaController::class, 'index'])->name('beranda');
|
||||
Route::get('/login', [App\Http\Controllers\Admin\AuthController::class, 'login'])->name('login');
|
||||
Route::get('/pesan-buket', [App\Http\Controllers\User\PesanBuketController::class, 'index'])->name('pesan.buket');
|
||||
Route::get('/detail-buket', [App\Http\Controllers\User\PesanBuketController::class, 'detail'])->name('detail.buket');
|
||||
Route::get('/formulir-pemesanan-buket', [App\Http\Controllers\User\PesanBuketController::class, 'formulir'])->name('formulir.buket');
|
||||
Route::get('/booking-foto', [App\Http\Controllers\User\BookingFotoController::class, 'index'])->name('booking.foto');
|
||||
Route::get('/detail-paket-foto', [App\Http\Controllers\User\BookingFotoController::class, 'detail'])->name('detail.foto');
|
||||
Route::get('/formulir-pemesanan-foto', [App\Http\Controllers\User\BookingFotoController::class, 'formulir'])->name('formulir.foto');
|
||||
Route::get('/', [User\BerandaController::class, 'index'])->name('beranda');
|
||||
|
||||
// Fitur Buket
|
||||
Route::get('/pesan-buket', [User\PesanBuketController::class, 'index'])->name('pesan.buket');
|
||||
Route::get('/pesan-buket/{id}', [User\PesanBuketController::class, 'detail'])->name('detail.buket');
|
||||
Route::get('/formulir-buket/{id}', [User\PesanBuketController::class, 'formulir'])->name('formulir.buket');
|
||||
Route::post('/formulir-buket/store', [User\PesanBuketController::class, 'store'])->name('transaksi.buket.store');
|
||||
// Fitur Foto
|
||||
Route::get('/booking-foto', [User\BookingFotoController::class, 'index'])->name('booking.foto');
|
||||
Route::get('/detail-paket-foto/{id}', [User\BookingFotoController::class, 'detail'])->name('detail.foto'); // Tambah {id}
|
||||
Route::get('/formulir-pemesanan-foto', [User\BookingFotoController::class, 'formulir'])->name('formulir.foto');
|
||||
|
||||
Route::get('/load-calendar', [User\BookingFotoController::class, 'loadCalendar'])->name('ajax.load-calendar');
|
||||
Route::get('/cek-slot-foto', [User\BookingFotoController::class, 'cekSlot'])->name('ajax.check-slot');
|
||||
|
||||
Route::post('/simpan-booking-foto', [User\BookingFotoController::class, 'store'])->name('transaksi.foto.store');
|
||||
Route::get('/cancel-booking', [User\BookingFotoController::class, 'cancelBooking'])->name('booking.cancel');
|
||||
|
||||
Route::get('/test-flow', [User\TestBookingController::class, 'index']);
|
||||
Route::get('/cek-slot-foto', [User\TestBookingController::class, 'cekSlot']);
|
||||
Route::post('/test-flow-simpan', [User\TestBookingController::class, 'store'])->name('test.simpan.foto');
|
||||
});
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 2. AUTENTIKASI (Login/Logout)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Menampilkan form login
|
||||
Route::get('/login', [Admin\AuthController::class, 'login'])->name('login')->middleware('guest');
|
||||
// Memproses data login (YANG SEBELUMNYA KURANG)
|
||||
Route::post('/login', [Admin\AuthController::class, 'authenticate'])->name('login.proses');
|
||||
// Logout
|
||||
Route::post('/logout', [Admin\AuthController::class, 'logout'])->name('logout');
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 3. ADMIN & OWNER PANEL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
|
||||
|
||||
Route::redirect('/', '/admin/beranda');
|
||||
Route::get('/beranda', [Admin\BerandaController::class, 'admin'])->name('beranda');
|
||||
Route::get('/beranda-pemilik', [Admin\BerandaController::class, 'pemilik'])->name('beranda.pemilik');
|
||||
|
||||
Route::resource('pesanan-buket', Admin\PesananBuketController::class);
|
||||
Route::resource('pesanan-foto', Admin\PesananFotoController::class);
|
||||
// Dashboard
|
||||
Route::get('/beranda', [Admin\BerandaController::class, 'index'])->name('beranda');
|
||||
|
||||
Route::get('/pesanan-buket', [Admin\PesananBuketController::class, 'index'])
|
||||
->name('pesanan-buket.index');
|
||||
|
||||
// Rute Update Status (Put) - Pastikan nama ini unik
|
||||
Route::put('/pesanan-buket/update-status/{id}', [Admin\PesananBuketController::class, 'updateStatus'])
|
||||
->name('pesanan-buket.update-status');
|
||||
|
||||
Route::get('/pesanan-foto', [Admin\PesananFotoController::class, 'index'])
|
||||
->name('pesanan-foto.index');
|
||||
|
||||
// Rute Update Status (Put) - Pastikan nama ini unik
|
||||
Route::put('/pesanan-foto/update-status/{id}', [Admin\PesananFotoController::class, 'updateStatus'])
|
||||
->name('pesanan-foto.update-status');
|
||||
|
||||
Route::get('/riwayat-pesanan', [Admin\HistoriPesananController::class, 'index'])->name('riwayat');
|
||||
|
||||
Route::resource('produk-buket', App\Http\Controllers\Admin\BuketController::class);
|
||||
Route::resource('paket-foto', App\Http\Controllers\Admin\FotoController::class);
|
||||
|
||||
// --- 2. OWNER ONLY ROUTES (Khusus Pemilik) ---
|
||||
// Kita bungkus dengan logic middleware sederhana atau Gate
|
||||
// Route::group(['middleware' => function ($request, $next) {
|
||||
// // Cek: Kalau bukan pemilik, lempar error 403 (Forbidden)
|
||||
// if (auth()->user()->role !== 'pemilik') {
|
||||
// abort(403, 'Akses Ditolak. Halaman ini khusus Pemilik.');
|
||||
// }
|
||||
// return $next($request);
|
||||
// }], function () {
|
||||
|
||||
// // Menu ini hanya bisa dibuka Pemilik
|
||||
// Route::resource('kelola-admin', Admin\ManajemenAdminController::class);
|
||||
// });
|
||||
// Manajemen Produk (Master Data)
|
||||
Route::resource('produk-buket', Admin\BuketController::class); // Perbaiki namespace jika perlu
|
||||
Route::resource('paket-foto', Admin\FotoController::class); // Perbaiki namespace jika perlu
|
||||
Route::resource('additional', Admin\AdditionalController::class)->except(['index', 'show']);
|
||||
|
||||
// Manajemen User (Khusus Owner)
|
||||
Route::resource('kelola-admin', Admin\ManajemenAdminController::class);
|
||||
|
||||
// Profil Diri
|
||||
Route::resource('profil', Admin\ProfilController::class);
|
||||
Route::put('/profil/update', [Admin\ProfilController::class, 'update'])->name('profil.simpan');
|
||||
|
||||
Route::put('/profil/password', [Admin\ProfilController::class, 'updatePassword'])->name('profil.password');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue