Checkpoint
This commit is contained in:
parent
50accc8a00
commit
d59f1fb75e
|
@ -11,9 +11,9 @@
|
||||||
<a href="{{ route('admin.dashboard') }}" class="btn btn-secondary">
|
<a href="{{ route('admin.dashboard') }}" class="btn btn-secondary">
|
||||||
<i class="fas fa-arrow-left mr-1"></i> Kembali
|
<i class="fas fa-arrow-left mr-1"></i> Kembali
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.bookings.export') }}" class="btn btn-success">
|
{{-- <a href="{{ route('admin.bookings.export') }}" class="btn btn-success">
|
||||||
<i class="fas fa-file-excel mr-1"></i> Export Excel
|
<i class="fas fa-file-excel mr-1"></i> Export Excel
|
||||||
</a>
|
</a> --}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Cards - Row 1 -->
|
<!-- Stats Cards - Row 1: Revenue and Booking Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
<!-- Today's Revenue -->
|
<!-- Today's Revenue -->
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-green-500 hover:shadow-md transition">
|
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-green-500 hover:shadow-md transition">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
|
@ -59,57 +59,71 @@ class="font-semibold text-amber-500">{{ $pendingBookings }}</span></p>
|
||||||
class="font-semibold text-green-500">{{ $paidBookings }}</span></p>
|
class="font-semibold text-green-500">{{ $paidBookings }}</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Total Tables -->
|
<!-- Row 2: Top 5 Pelanggan Loyal Leaderboard - Full Width -->
|
||||||
|
<div class="mb-6">
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-purple-500 hover:shadow-md transition">
|
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-purple-500 hover:shadow-md transition">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<div>
|
<h2 class="text-lg font-semibold text-gray-800">🏆 Top 5 Pelanggan Loyal</h2>
|
||||||
<p class="text-sm font-medium text-gray-500">Total Meja</p>
|
|
||||||
<p class="text-2xl font-bold text-gray-800">{{ $totalTables }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-purple-500 p-2 bg-purple-50 rounded-lg">
|
<div class="text-purple-500 p-2 bg-purple-50 rounded-lg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M4 6h16M4 12h16M4 18h7" />
|
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 space-x-4">
|
|
||||||
<p class="text-xs text-gray-500">Tersedia: <span
|
|
||||||
class="font-semibold text-green-500">{{ $availableTables }}</span></p>
|
|
||||||
<p class="text-xs text-gray-500">Digunakan: <span
|
|
||||||
class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table Usage -->
|
@if(!empty($topUsers) && count($topUsers) > 0)
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-amber-500 hover:shadow-md transition">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
<div class="flex justify-between items-start">
|
@foreach($topUsers->take(5) as $index => $user)
|
||||||
<div>
|
<div
|
||||||
<p class="text-sm font-medium text-gray-500">Penggunaan Meja</p>
|
class="flex flex-col items-center p-4 {{ $index === 0 ? 'bg-gradient-to-br from-yellow-50 to-yellow-100 border-2 border-yellow-300' : ($index === 1 ? 'bg-gradient-to-br from-gray-50 to-gray-100 border-2 border-gray-300' : 'bg-gradient-to-br from-orange-50 to-orange-100 border-2 border-orange-300') }} rounded-lg text-center">
|
||||||
<p class="text-2xl font-bold text-gray-800">
|
<div class="mb-3">
|
||||||
{{ $totalTables > 0 ? round(($usedTables / $totalTables) * 100) : 0 }}%
|
@if($index === 0)
|
||||||
</p>
|
<span
|
||||||
|
class="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-600 text-white rounded-full text-2xl font-bold shadow-lg">🥇</span>
|
||||||
|
@elseif($index === 1)
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-gray-300 to-gray-500 text-white rounded-full text-2xl font-bold shadow-lg">🥈</span>
|
||||||
|
@else
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-orange-400 to-orange-600 text-white rounded-full text-2xl font-bold shadow-lg">🥉</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<h3 class="font-bold text-sm text-gray-800 mb-1">{{ Str::limit($user['user_name'], 12) }}
|
||||||
|
</h3>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
Ranking #{{ $index + 1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-full px-3 py-1 shadow-sm">
|
||||||
|
<p class="text-lg font-bold text-gray-800">{{ $user['booking_count'] }}</p>
|
||||||
|
<p class="text-xs text-gray-500">Booking</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
<div class="text-amber-500 p-2 bg-amber-50 rounded-lg">
|
@else
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
<div class="text-center py-8">
|
||||||
stroke="currentColor">
|
<div class="text-gray-400 mb-2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto" fill="none"
|
||||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-sm">Belum ada data pelanggan loyal</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endif
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-3">
|
|
||||||
<div class="bg-amber-500 h-2.5 rounded-full"
|
|
||||||
style="width: {{ $totalTables > 0 ? ($usedTables / $totalTables) * 100 : 0 }}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Performance & Trends Section -->
|
<!-- Main Performance Section -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
<!-- Monthly Revenue Chart - MOVED UP (was 6 months, now showing 12 months) -->
|
<!-- Monthly Revenue Chart -->
|
||||||
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-6">
|
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-6">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="font-semibold text-lg">Tren Pendapatan Bulanan</h2>
|
<h2 class="font-semibold text-lg">Tren Pendapatan Bulanan</h2>
|
||||||
|
@ -142,10 +156,11 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
<p class="font-medium text-gray-800">{{ $booking->user->name }}</p>
|
<p class="font-medium text-gray-800">{{ $booking->user->name }}</p>
|
||||||
<div class="flex items-center text-sm text-gray-500">
|
<div class="flex items-center text-sm text-gray-500">
|
||||||
<span class="mr-2">{{ $booking->table->name }}</span>
|
<span class="mr-2">{{ $booking->table->name }}</span>
|
||||||
<span class="text-xs px-2 py-0.5 rounded-full {{
|
<span
|
||||||
$booking->status === 'paid' ? 'bg-green-100 text-green-800' :
|
class="text-xs px-2 py-0.5 rounded-full {{
|
||||||
|
$booking->status === 'paid' ? 'bg-green-100 text-green-800' :
|
||||||
($booking->status === 'pending' ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-800')
|
($booking->status === 'pending' ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-800')
|
||||||
}}">
|
}}">
|
||||||
{{ ucfirst($booking->status) }}
|
{{ ucfirst($booking->status) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,7 +184,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
<div class="h-80" id="weeklyRevenueChart"></div>
|
<div class="h-80" id="weeklyRevenueChart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Revenue Performance - MOVED UP -->
|
<!-- Table Revenue Performance -->
|
||||||
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-6">
|
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-6">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="font-semibold text-lg">Performa Pendapatan per Meja (Bulan Ini)</h2>
|
<h2 class="font-semibold text-lg">Performa Pendapatan per Meja (Bulan Ini)</h2>
|
||||||
|
@ -177,14 +192,6 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
<div class="h-80" id="tableRevenueChart"></div>
|
<div class="h-80" id="tableRevenueChart"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Customer Insights Section -->
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="font-semibold text-lg">Top 5 Pelanggan Loyal</h2>
|
|
||||||
</div>
|
|
||||||
<div class="h-80" id="topUsersChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -192,7 +199,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Monthly Revenue Chart - ENHANCED TO SHOW 12 MONTHS
|
// Monthly Revenue Chart
|
||||||
var monthlyRevenueData = @json($lastSixMonthsRevenue);
|
var monthlyRevenueData = @json($lastSixMonthsRevenue);
|
||||||
|
|
||||||
var monthlyRevenueOptions = {
|
var monthlyRevenueOptions = {
|
||||||
|
@ -336,7 +343,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
var chart = new ApexCharts(document.querySelector("#weeklyRevenueChart"), options);
|
var chart = new ApexCharts(document.querySelector("#weeklyRevenueChart"), options);
|
||||||
chart.render();
|
chart.render();
|
||||||
|
|
||||||
// Table Revenue Performance Chart - ENHANCED WITH IMPROVED VISUALS
|
// Table Revenue Performance Chart
|
||||||
var tableRevenueData = @json($tableRevenue);
|
var tableRevenueData = @json($tableRevenue);
|
||||||
|
|
||||||
// Verifikasi data tersedia dan lengkap
|
// Verifikasi data tersedia dan lengkap
|
||||||
|
@ -432,95 +439,6 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
|
||||||
var tableRevenueChart = new ApexCharts(document.querySelector("#tableRevenueChart"), tableRevenueOptions);
|
var tableRevenueChart = new ApexCharts(document.querySelector("#tableRevenueChart"), tableRevenueOptions);
|
||||||
tableRevenueChart.render();
|
tableRevenueChart.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top 5 Users Chart - ENHANCED WITH IMPROVED VISUALS
|
|
||||||
var topUsersData = @json($topUsers);
|
|
||||||
|
|
||||||
// Verifikasi data tersedia dan lengkap
|
|
||||||
if (!topUsersData || topUsersData.length === 0) {
|
|
||||||
document.getElementById("topUsersChart").innerHTML =
|
|
||||||
'<div class="flex items-center justify-center h-full"><p class="text-gray-500">Tidak ada data tersedia</p></div>';
|
|
||||||
} else {
|
|
||||||
var topUsersOptions = {
|
|
||||||
series: [{
|
|
||||||
name: 'Jumlah Booking',
|
|
||||||
data: topUsersData.map(item => item.booking_count)
|
|
||||||
}],
|
|
||||||
chart: {
|
|
||||||
type: 'bar',
|
|
||||||
height: 300,
|
|
||||||
toolbar: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
borderRadius: 4,
|
|
||||||
horizontal: true,
|
|
||||||
distributed: true,
|
|
||||||
dataLabels: {
|
|
||||||
position: 'top'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors: ['#8b5cf6', '#6366f1', '#4f46e5', '#4338ca', '#3730a3'],
|
|
||||||
dataLabels: {
|
|
||||||
enabled: true,
|
|
||||||
formatter: function (val) {
|
|
||||||
return val + ' booking';
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
fontSize: '12px',
|
|
||||||
colors: ['#304758']
|
|
||||||
},
|
|
||||||
offsetX: 30
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: topUsersData.map(item => item.user_name),
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
fontSize: '12px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: 'Pelanggan'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
y: {
|
|
||||||
formatter: function (val) {
|
|
||||||
return val + ' booking';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
opacity: 1,
|
|
||||||
type: 'gradient',
|
|
||||||
gradient: {
|
|
||||||
shade: 'dark',
|
|
||||||
type: "horizontal",
|
|
||||||
shadeIntensity: 0.5,
|
|
||||||
inverseColors: true,
|
|
||||||
opacityFrom: 1,
|
|
||||||
opacityTo: 0.8,
|
|
||||||
stops: [0, 100]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: 'Pelanggan dengan Booking Terbanyak',
|
|
||||||
align: 'center',
|
|
||||||
style: {
|
|
||||||
fontSize: '18px',
|
|
||||||
fontWeight: 'medium'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var topUsersChart = new ApexCharts(document.querySelector("#topUsersChart"), topUsersOptions);
|
|
||||||
topUsersChart.render();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
|
@ -117,7 +117,7 @@ class="inline-flex items-center px-3 py-1.5 border border-transparent rounded-md
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart: Usage Patterns -->
|
{{-- <!-- Chart: Usage Patterns -->
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
|
<div class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
|
||||||
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
|
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
|
||||||
|
@ -127,7 +127,7 @@ class="inline-flex items-center px-3 py-1.5 border border-transparent rounded-md
|
||||||
<canvas id="usagePatternChart" height="300"></canvas>
|
<canvas id="usagePatternChart" height="300"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> --}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -127,7 +127,7 @@ class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div x-show="sidebarOpen"
|
{{-- <div x-show="sidebarOpen"
|
||||||
class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2 mt-6">
|
class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2 mt-6">
|
||||||
Sistem
|
Sistem
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,7 +153,7 @@ class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2 mt
|
||||||
</svg>
|
</svg>
|
||||||
<span x-show="sidebarOpen">Pengaturan</span>
|
<span x-show="sidebarOpen">Pengaturan</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav> --}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Profile -->
|
<!-- User Profile -->
|
||||||
|
@ -179,8 +179,8 @@ class="ml-auto h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="cu
|
||||||
<!-- Dropdown -->
|
<!-- Dropdown -->
|
||||||
<div x-show="open" @click.outside="open = false"
|
<div x-show="open" @click.outside="open = false"
|
||||||
class="absolute bottom-full left-0 mb-1 w-full bg-white rounded-lg shadow-lg border border-gray-200 py-1 dropdown-transition">
|
class="absolute bottom-full left-0 mb-1 w-full bg-white rounded-lg shadow-lg border border-gray-200 py-1 dropdown-transition">
|
||||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
|
{{-- <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
|
||||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a>
|
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a> --}}
|
||||||
<div class="border-t border-gray-200 my-1"></div>
|
<div class="border-t border-gray-200 my-1"></div>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
|
@ -223,14 +223,14 @@ class="absolute right-0 mt-2 w-56 bg-white rounded-xl shadow-lg border border-gr
|
||||||
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Admin' }}</p>
|
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Admin' }}</p>
|
||||||
<p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'admin@example.com' }}</p>
|
<p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'admin@example.com' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
{{-- <a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
<i class="fas fa-user mr-3 text-gray-400"></i>
|
<i class="fas fa-user mr-3 text-gray-400"></i>
|
||||||
Profile Settings
|
Profile Settings
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
<i class="fas fa-cog mr-3 text-gray-400"></i>
|
<i class="fas fa-cog mr-3 text-gray-400"></i>
|
||||||
Account Settings
|
Account Settings
|
||||||
</a>
|
</a> --}}
|
||||||
<div class="border-t border-gray-100 mt-2 pt-2">
|
<div class="border-t border-gray-100 mt-2 pt-2">
|
||||||
<a href="{{ route('logout') }}"
|
<a href="{{ route('logout') }}"
|
||||||
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
|
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
|
||||||
|
|
|
@ -51,7 +51,7 @@ class="w-full py-3 md:px-6 rounded-lg text-sm bg-primary text-white font-semibol
|
||||||
@forelse ($venues as $venue)
|
@forelse ($venues as $venue)
|
||||||
<a href="{{ route('venue', ['venueName' => $venue->name]) }}"
|
<a href="{{ route('venue', ['venueName' => $venue->name]) }}"
|
||||||
class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden">
|
class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden">
|
||||||
<img src="{{ $venue->image }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover">
|
<img src="{{ Storage::url($venue->image) }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover">
|
||||||
<div class="flex-grow px-4 py-2">
|
<div class="flex-grow px-4 py-2">
|
||||||
<h3 class="text-sm text-gray-400 font-semibold mb-2">Venue</h3>
|
<h3 class="text-sm text-gray-400 font-semibold mb-2">Venue</h3>
|
||||||
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue->name }}</h1>
|
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue->name }}</h1>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
@extends('layouts.main')
|
@extends('layouts.main')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
|
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
|
||||||
<h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
|
<!-- Notification Container -->
|
||||||
|
<div id="notification-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<h2 class="text-lg font-semibold mb-4">Detail Booking Saat Ini</h2>
|
<h2 class="text-lg font-semibold mb-4">Detail Booking Saat Ini</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
@ -26,83 +30,145 @@
|
||||||
<p class="font-medium">{{ $duration }} Jam</p>
|
<p class="font-medium">{{ $duration }} Jam</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
||||||
<div class="flex items-start">
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
<div class="mr-3 text-yellow-500">
|
<div class="flex items-start">
|
||||||
<i class="fa-solid fa-exclamation-circle text-xl"></i>
|
<div class="mr-3 text-yellow-500">
|
||||||
|
<i class="fa-solid fa-exclamation-circle text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-yellow-700">Perhatian</h3>
|
||||||
|
<p class="text-yellow-700 text-sm">
|
||||||
|
• Reschedule dapat dilakukan selama minimal 1 jam sebelum jadwal booking<br>
|
||||||
|
• Setiap booking hanya dapat di-reschedule maksimal 1 kali<br>
|
||||||
|
• Durasi booking akan tetap sama ({{ $duration }} jam)<br>
|
||||||
|
• Setelah reschedule, jadwal lama akan digantikan dengan jadwal baru
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-data="rescheduleForm" class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Pilih Jadwal Baru</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold text-yellow-700">Perhatian</h3>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Tanggal:</label>
|
||||||
<p class="text-yellow-700 text-sm">
|
<input type="date" x-model="selectedDate" class="w-full border p-2 rounded-lg"
|
||||||
• Reschedule dapat dilakukan selama minimal 1 jam sebelum jadwal booking<br>
|
:min="today" @change="dateChanged">
|
||||||
• Setiap booking hanya dapat di-reschedule maksimal 1 kali<br>
|
</div>
|
||||||
• Durasi booking akan tetap sama ({{ $duration }} jam)<br>
|
|
||||||
• Setelah reschedule, jadwal lama akan digantikan dengan jadwal baru
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
|
||||||
|
<select x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="tableChanged">
|
||||||
|
<option value="">-- Pilih Meja --</option>
|
||||||
|
<template x-for="table in tables" :key="table.id">
|
||||||
|
<option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6" x-show="selectedDate && selectedTableId">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label>
|
||||||
|
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||||
|
<template x-for="hour in availableHours" :key="hour">
|
||||||
|
<button
|
||||||
|
class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
|
||||||
|
:class="isTimeSlotAvailable(hour) ?
|
||||||
|
(selectedStartHour === hour ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800') :
|
||||||
|
'bg-gray-200 text-gray-400 cursor-not-allowed opacity-70'"
|
||||||
|
:disabled="!isTimeSlotAvailable(hour)"
|
||||||
|
@click="selectStartHour(hour)"
|
||||||
|
x-text="hour + ':00'">
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4" x-show="selectedStartHour">
|
||||||
|
<p class="text-sm text-gray-700 mb-2">
|
||||||
|
Jadwal reschedule: <span class="font-medium" x-text="formattedSchedule"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-end">
|
||||||
|
<a href="{{ route('booking.history') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg mr-3">
|
||||||
|
Batal
|
||||||
|
</a>
|
||||||
|
<button @click="submitReschedule"
|
||||||
|
:disabled="!canSubmit || isSubmitting"
|
||||||
|
:class="canSubmit ? 'bg-green-500 hover:bg-green-600' : 'bg-green-300 cursor-not-allowed'"
|
||||||
|
class="text-white px-4 py-2 rounded-lg">
|
||||||
|
<span x-show="!isSubmitting">Konfirmasi Reschedule</span>
|
||||||
|
<span x-show="isSubmitting">Memproses...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-data="rescheduleForm" class="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4">Pilih Jadwal Baru</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Tanggal:</label>
|
|
||||||
<input type="date" x-model="selectedDate" class="w-full border p-2 rounded-lg"
|
|
||||||
:min="today" @change="dateChanged">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
|
|
||||||
<select x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="tableChanged">
|
|
||||||
<option value="">-- Pilih Meja --</option>
|
|
||||||
<template x-for="table in tables" :key="table.id">
|
|
||||||
<option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6" x-show="selectedDate && selectedTableId">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label>
|
|
||||||
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
|
||||||
<template x-for="hour in availableHours" :key="hour">
|
|
||||||
<button
|
|
||||||
class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
|
|
||||||
:class="isTimeSlotAvailable(hour) ?
|
|
||||||
(selectedStartHour === hour ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800') :
|
|
||||||
'bg-gray-200 text-gray-400 cursor-not-allowed opacity-70'"
|
|
||||||
:disabled="!isTimeSlotAvailable(hour)"
|
|
||||||
@click="selectStartHour(hour)"
|
|
||||||
x-text="hour + ':00'">
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4" x-show="selectedStartHour">
|
|
||||||
<p class="text-sm text-gray-700 mb-2">
|
|
||||||
Jadwal reschedule: <span class="font-medium" x-text="formattedSchedule"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 flex justify-end">
|
|
||||||
<a href="{{ route('booking.history') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg mr-3">
|
|
||||||
Batal
|
|
||||||
</a>
|
|
||||||
<button @click="submitReschedule"
|
|
||||||
:disabled="!canSubmit || isSubmitting"
|
|
||||||
:class="canSubmit ? 'bg-green-500 hover:bg-green-600' : 'bg-green-300 cursor-not-allowed'"
|
|
||||||
class="text-white px-4 py-2 rounded-lg">
|
|
||||||
<span x-show="!isSubmitting">Konfirmasi Reschedule</span>
|
|
||||||
<span x-show="isSubmitting">Memproses...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
<script>
|
||||||
|
// Notification System
|
||||||
|
function showNotification(message, type = 'info', duration = 5000) {
|
||||||
|
const container = document.getElementById('notification-container');
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
|
||||||
|
// Set notification styles based on type
|
||||||
|
let bgColor, textColor, icon;
|
||||||
|
switch(type) {
|
||||||
|
case 'success':
|
||||||
|
bgColor = 'bg-green-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
icon = 'fa-check-circle';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
bgColor = 'bg-red-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
icon = 'fa-exclamation-circle';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
bgColor = 'bg-yellow-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
icon = 'fa-exclamation-triangle';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
bgColor = 'bg-blue-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
icon = 'fa-info-circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.className = `${bgColor} ${textColor} px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 ease-in-out opacity-0 translate-x-full flex items-center space-x-3 max-w-md`;
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<i class="fas ${icon} text-lg"></i>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="font-medium">${message}</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200 focus:outline-none">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(notification);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0', 'translate-x-full');
|
||||||
|
notification.classList.add('opacity-100', 'translate-x-0');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Auto remove after duration
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('opacity-0', 'translate-x-full');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentElement) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('rescheduleForm', () => ({
|
Alpine.data('rescheduleForm', () => ({
|
||||||
tables: @json($venue->tables),
|
tables: @json($venue->tables),
|
||||||
|
@ -115,22 +181,19 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
selectedTableId: '',
|
selectedTableId: '',
|
||||||
selectedStartHour: null,
|
selectedStartHour: null,
|
||||||
bookedSchedules: [],
|
bookedSchedules: [],
|
||||||
availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')), // 9AM to 24PM
|
availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')),
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Set today as default value
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const year = today.getFullYear();
|
const year = today.getFullYear();
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
this.today = `${year}-${month}-${day}`;
|
this.today = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
// Set original date and table as default
|
|
||||||
this.selectedDate = this.originalDate;
|
this.selectedDate = this.originalDate;
|
||||||
this.selectedTableId = this.originalTableId;
|
this.selectedTableId = this.originalTableId;
|
||||||
|
|
||||||
// Load schedules for today and selected table
|
|
||||||
this.checkBookedSchedules();
|
this.checkBookedSchedules();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -138,7 +201,6 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
return this.selectedDate &&
|
return this.selectedDate &&
|
||||||
this.selectedTableId &&
|
this.selectedTableId &&
|
||||||
this.selectedStartHour !== null &&
|
this.selectedStartHour !== null &&
|
||||||
// Prevent submitting if nothing changed
|
|
||||||
(this.selectedDate !== this.originalDate ||
|
(this.selectedDate !== this.originalDate ||
|
||||||
this.selectedTableId != this.originalTableId ||
|
this.selectedTableId != this.originalTableId ||
|
||||||
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
|
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
|
||||||
|
@ -173,8 +235,6 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
}
|
}
|
||||||
this.bookedSchedules = await response.json();
|
this.bookedSchedules = await response.json();
|
||||||
|
|
||||||
// If today is the original booking date and table is the original table,
|
|
||||||
// pre-select the original start hour (only if it's still valid)
|
|
||||||
if (this.selectedDate === this.originalDate &&
|
if (this.selectedDate === this.originalDate &&
|
||||||
parseInt(this.selectedTableId) === parseInt(this.originalTableId)) {
|
parseInt(this.selectedTableId) === parseInt(this.originalTableId)) {
|
||||||
const originalHour = this.originalStartTime.split(':')[0];
|
const originalHour = this.originalStartTime.split(':')[0];
|
||||||
|
@ -184,7 +244,7 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking booked schedules:', error);
|
console.error('Error checking booked schedules:', error);
|
||||||
alert('Terjadi kesalahan saat memeriksa jadwal. Silakan coba lagi.');
|
showNotification('Terjadi kesalahan saat memeriksa jadwal. Silakan coba lagi.', 'error');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -192,29 +252,24 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
const hourInt = parseInt(hour);
|
const hourInt = parseInt(hour);
|
||||||
const endHourInt = hourInt + this.bookingDuration;
|
const endHourInt = hourInt + this.bookingDuration;
|
||||||
|
|
||||||
// Check if slot end time exceeds midnight
|
|
||||||
if (endHourInt > 24) return false;
|
if (endHourInt > 24) return false;
|
||||||
|
|
||||||
// Check if selected date is today and hour has passed
|
|
||||||
const selectedDate = new Date(this.selectedDate);
|
const selectedDate = new Date(this.selectedDate);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const isToday = selectedDate.toDateString() === today.toDateString();
|
const isToday = selectedDate.toDateString() === today.toDateString();
|
||||||
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
const currentHour = today.getHours();
|
const currentHour = today.getHours();
|
||||||
// If the selected hour has already passed today, disable it
|
|
||||||
if (hourInt <= currentHour) {
|
if (hourInt <= currentHour) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is the original booking's time slot
|
|
||||||
const isOriginalTimeSlot = this.selectedDate === this.originalDate &&
|
const isOriginalTimeSlot = this.selectedDate === this.originalDate &&
|
||||||
parseInt(this.selectedTableId) === parseInt(this.originalTableId) &&
|
parseInt(this.selectedTableId) === parseInt(this.originalTableId) &&
|
||||||
hour === this.originalStartTime.split(':')[0];
|
hour === this.originalStartTime.split(':')[0];
|
||||||
|
|
||||||
if (isOriginalTimeSlot) {
|
if (isOriginalTimeSlot) {
|
||||||
// Allow original time slot only if it's not in the past
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
const currentHour = today.getHours();
|
const currentHour = today.getHours();
|
||||||
return hourInt > currentHour;
|
return hourInt > currentHour;
|
||||||
|
@ -222,12 +277,10 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any existing booking overlaps with this slot
|
|
||||||
return !this.bookedSchedules.some(schedule => {
|
return !this.bookedSchedules.some(schedule => {
|
||||||
const scheduleStart = parseInt(schedule.start.split(':')[0]);
|
const scheduleStart = parseInt(schedule.start.split(':')[0]);
|
||||||
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
|
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
|
||||||
|
|
||||||
// Check if there's overlap
|
|
||||||
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
|
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -243,11 +296,9 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
|
|
||||||
this.isSubmitting = true;
|
this.isSubmitting = true;
|
||||||
|
|
||||||
// Calculate end time based on selected start hour and duration
|
|
||||||
const startHour = parseInt(this.selectedStartHour);
|
const startHour = parseInt(this.selectedStartHour);
|
||||||
const endHour = startHour + this.bookingDuration;
|
const endHour = startHour + this.bookingDuration;
|
||||||
|
|
||||||
// Format date strings for API
|
|
||||||
const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
|
const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
|
||||||
const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
|
const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
|
||||||
|
|
||||||
|
@ -268,15 +319,17 @@ class="text-white px-4 py-2 rounded-lg">
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(result.message);
|
showNotification(result.message, 'success');
|
||||||
window.location.href = result.redirect;
|
setTimeout(() => {
|
||||||
|
window.location.href = result.redirect;
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
alert(result.message || 'Terjadi kesalahan saat memproses reschedule.');
|
showNotification(result.message || 'Terjadi kesalahan saat memproses reschedule.', 'error');
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting reschedule:', error);
|
console.error('Error submitting reschedule:', error);
|
||||||
alert('Terjadi kesalahan. Silakan coba lagi.');
|
showNotification('Terjadi kesalahan. Silakan coba lagi.', 'error');
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
@extends('layouts.main')
|
@extends('layouts.main')
|
||||||
@section('content')
|
@section('content')
|
||||||
|
<!-- Toast Notification Container -->
|
||||||
|
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div x-data="{ show: false }" x-show="show" x-cloak x-on:show-loading.window="show = true"
|
||||||
|
x-on:hide-loading.window="show = false"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
|
||||||
|
<div class="bg-white rounded-lg p-6 shadow-xl">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
<span class="text-gray-700">Memproses booking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto">
|
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<img src="{{ asset($venue['image']) }}" alt="{{ $venue['name'] }}" class="w-full rounded-lg mb-4 mt-8">
|
<img src="{{ Storage::url($venue['image']) }}" alt="{{ $venue['name'] }}"
|
||||||
|
class="w-full h-full object-cover rounded-lg mb-4 mt-8" />
|
||||||
|
|
||||||
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
|
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
|
||||||
<p class="text-sm text-gray-500">{{ $venue['location'] }}</p>
|
<p class="text-sm text-gray-500">{{ $venue['location'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,17 +157,171 @@ class="border rounded-lg shadow-md p-4 mb-4">
|
||||||
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
|
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
|
||||||
data-client-key="{{ config('midtrans.client_key') }}"></script>
|
data-client-key="{{ config('midtrans.client_key') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Toast Notification Function
|
||||||
|
function showToast(message, type = 'info', duration = 5000) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
|
||||||
|
const bgColor = {
|
||||||
|
'success': 'bg-green-500',
|
||||||
|
'error': 'bg-red-500',
|
||||||
|
'warning': 'bg-yellow-500',
|
||||||
|
'info': 'bg-blue-500'
|
||||||
|
}[type] || 'bg-blue-500';
|
||||||
|
|
||||||
|
const icon = {
|
||||||
|
'success': 'fa-check-circle',
|
||||||
|
'error': 'fa-exclamation-circle',
|
||||||
|
'warning': 'fa-exclamation-triangle',
|
||||||
|
'info': 'fa-info-circle'
|
||||||
|
}[type] || 'fa-info-circle';
|
||||||
|
|
||||||
|
toast.className = `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center space-x-3 min-w-80 transform transition-all duration-300 translate-x-full opacity-0`;
|
||||||
|
toast.innerHTML = `
|
||||||
|
<i class="fas ${icon}"></i>
|
||||||
|
<span class="flex-1">${message}</span>
|
||||||
|
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-x-full', 'opacity-0');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Auto remove
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('translate-x-full', 'opacity-0');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Notification Function
|
||||||
|
function showModal(title, message, type = 'info', callback = null) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
||||||
|
|
||||||
|
const iconColor = {
|
||||||
|
'success': 'text-green-500',
|
||||||
|
'error': 'text-red-500',
|
||||||
|
'warning': 'text-yellow-500',
|
||||||
|
'info': 'text-blue-500'
|
||||||
|
}[type] || 'text-blue-500';
|
||||||
|
|
||||||
|
const icon = {
|
||||||
|
'success': 'fa-check-circle',
|
||||||
|
'error': 'fa-exclamation-circle',
|
||||||
|
'warning': 'fa-exclamation-triangle',
|
||||||
|
'info': 'fa-info-circle'
|
||||||
|
}[type] || 'fa-info-circle';
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all">
|
||||||
|
<div class="flex items-center space-x-3 mb-4">
|
||||||
|
<i class="fas ${icon} text-2xl ${iconColor}"></i>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-6">${message}</p>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button onclick="this.closest('.fixed').remove(); ${callback ? callback + '()' : ''}"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.remove();
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation Modal Function
|
||||||
|
function showConfirmModal(title, message, onConfirm, onCancel = null) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all">
|
||||||
|
<div class="flex items-center space-x-3 mb-4">
|
||||||
|
<i class="fas fa-question-circle text-2xl text-yellow-500"></i>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-6">${message}</p>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button id="cancelBtn" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 transition-colors">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button id="confirmBtn" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors">
|
||||||
|
Ya, Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const confirmBtn = modal.querySelector('#confirmBtn');
|
||||||
|
const cancelBtn = modal.querySelector('#cancelBtn');
|
||||||
|
|
||||||
|
confirmBtn.addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
if (onConfirm) onConfirm();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.remove();
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Custom event for refreshing pending bookings across components
|
// Custom event for refreshing pending bookings across components
|
||||||
const refreshPendingBookingsEvent = new Event('refresh-pending-bookings');
|
const refreshPendingBookingsEvent = new Event('refresh-pending-bookings');
|
||||||
|
|
||||||
function updateClock() {
|
// Tambahkan fungsi helper untuk mendapatkan tanggal Jakarta yang konsisten
|
||||||
|
function getJakartaDate() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const options = { timeZone: 'Asia/Jakarta', hour12: false };
|
// Buat objek Date dengan timezone Jakarta
|
||||||
const timeFormatter = new Intl.DateTimeFormat('id-ID', { ...options, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" }));
|
||||||
document.getElementById('realTimeClock').textContent = timeFormatter.format(now);
|
return jakartaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJakartaDateString() {
|
||||||
|
const jakartaTime = getJakartaDate();
|
||||||
|
const year = jakartaTime.getFullYear();
|
||||||
|
const month = String(jakartaTime.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(jakartaTime.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
const jakartaTime = getJakartaDate();
|
||||||
|
const timeFormatter = new Intl.DateTimeFormat('id-ID', {
|
||||||
|
timeZone: 'Asia/Jakarta',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
document.getElementById('realTimeClock').textContent = timeFormatter.format(jakartaTime);
|
||||||
}
|
}
|
||||||
setInterval(updateClock, 1000);
|
|
||||||
updateClock();
|
|
||||||
|
|
||||||
// Format functions for pending bookings
|
// Format functions for pending bookings
|
||||||
function formatDateTime(dateTimeStr) {
|
function formatDateTime(dateTimeStr) {
|
||||||
|
@ -244,9 +415,13 @@ function formatPrice(price) {
|
||||||
|
|
||||||
resumeBooking(bookingId) {
|
resumeBooking(bookingId) {
|
||||||
this.isLoadingPending = true;
|
this.isLoadingPending = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('show-loading'));
|
||||||
|
|
||||||
fetch(`/booking/pending/${bookingId}/resume`)
|
fetch(`/booking/pending/${bookingId}/resume`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log("Opening payment with snap token:", data.snap_token);
|
console.log("Opening payment with snap token:", data.snap_token);
|
||||||
// Open Snap payment
|
// Open Snap payment
|
||||||
|
@ -255,61 +430,68 @@ function formatPrice(price) {
|
||||||
this.createBookingAfterPayment(data.order_id, result);
|
this.createBookingAfterPayment(data.order_id, result);
|
||||||
},
|
},
|
||||||
onPending: (result) => {
|
onPending: (result) => {
|
||||||
alert('Pembayaran pending, silahkan selesaikan pembayaran');
|
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
},
|
},
|
||||||
onError: (result) => {
|
onError: (result) => {
|
||||||
alert('Pembayaran gagal');
|
showToast('Pembayaran gagal', 'error');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
|
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert(data.message);
|
showToast(data.message, 'error');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
// Refresh pending bookings list
|
// Refresh pending bookings list
|
||||||
this.fetchPendingBookings();
|
this.fetchPendingBookings();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
console.error('Error resuming booking:', error);
|
console.error('Error resuming booking:', error);
|
||||||
alert('Gagal melanjutkan booking');
|
showToast('Gagal melanjutkan booking', 'error');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deletePendingBooking(bookingId) {
|
deletePendingBooking(bookingId) {
|
||||||
if (confirm('Apakah Anda yakin ingin menghapus booking ini?')) {
|
showConfirmModal(
|
||||||
this.isLoadingPending = true;
|
'Konfirmasi Hapus',
|
||||||
fetch(`/booking/pending/${bookingId}`, {
|
'Apakah Anda yakin ingin menghapus booking ini?',
|
||||||
method: 'DELETE',
|
() => {
|
||||||
headers: {
|
this.isLoadingPending = true;
|
||||||
'Content-Type': 'application/json',
|
fetch(`/booking/pending/${bookingId}`, {
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
method: 'DELETE',
|
||||||
}
|
headers: {
|
||||||
})
|
'Content-Type': 'application/json',
|
||||||
.then(response => response.json())
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
alert('Booking berhasil dihapus');
|
|
||||||
this.fetchPendingBookings();
|
|
||||||
} else {
|
|
||||||
alert(data.message);
|
|
||||||
}
|
}
|
||||||
this.isLoadingPending = false;
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.then(response => response.json())
|
||||||
console.error('Error deleting booking:', error);
|
.then(data => {
|
||||||
alert('Gagal menghapus booking');
|
if (data.success) {
|
||||||
this.isLoadingPending = false;
|
showToast('Booking berhasil dihapus', 'success');
|
||||||
});
|
this.fetchPendingBookings();
|
||||||
}
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
this.isLoadingPending = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting booking:', error);
|
||||||
|
showToast('Gagal menghapus booking', 'error');
|
||||||
|
this.isLoadingPending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
createBookingAfterPayment(orderId, paymentResult) {
|
createBookingAfterPayment(orderId, paymentResult) {
|
||||||
|
window.dispatchEvent(new CustomEvent('show-loading'));
|
||||||
|
|
||||||
fetch('/booking', {
|
fetch('/booking', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -332,18 +514,22 @@ function formatPrice(price) {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
alert('Pembayaran dan booking berhasil!');
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
|
showToast('Pembayaran dan booking berhasil!', 'success');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
|
|
||||||
// Refresh pending bookings list
|
// Refresh pending bookings list
|
||||||
this.fetchPendingBookings();
|
this.fetchPendingBookings();
|
||||||
|
|
||||||
// Redirect to booking history
|
// Redirect to booking history
|
||||||
window.location.href = '/booking/history';
|
setTimeout(() => {
|
||||||
|
window.location.href = '/booking/history';
|
||||||
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
console.error('Booking error:', err);
|
console.error('Booking error:', err);
|
||||||
alert('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message);
|
showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
|
||||||
this.isLoadingPending = false;
|
this.isLoadingPending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -375,10 +561,13 @@ function formatPrice(price) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkBookedSchedules() {
|
async checkBookedSchedules() {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
// Gunakan tanggal Jakarta yang konsisten
|
||||||
|
const today = getJakartaDateString();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
|
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
|
||||||
this.bookedSchedules = await response.json();
|
this.bookedSchedules = await response.json();
|
||||||
|
console.log('Checking schedules for date:', today, 'Table:', this.tableId);
|
||||||
|
console.log('Booked schedules:', this.bookedSchedules);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking booked schedules:', error);
|
console.error('Error checking booked schedules:', error);
|
||||||
}
|
}
|
||||||
|
@ -386,30 +575,31 @@ function formatPrice(price) {
|
||||||
|
|
||||||
initiateBooking(tableId, tableName) {
|
initiateBooking(tableId, tableName) {
|
||||||
if (!this.isLoggedIn) {
|
if (!this.isLoggedIn) {
|
||||||
alert('Silahkan login terlebih dahulu untuk melakukan booking.');
|
showToast('Silahkan login terlebih dahulu untuk melakukan booking', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedTime = this.selectedTime;
|
const selectedTime = this.selectedTime;
|
||||||
const selectedDuration = this.selectedDuration;
|
const selectedDuration = this.selectedDuration;
|
||||||
|
|
||||||
if (!selectedTime || !selectedDuration) {
|
if (!selectedTime || !selectedDuration) {
|
||||||
alert('Please select both time and duration');
|
showToast('Please select both time and duration', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi jam
|
// Validasi jam menggunakan waktu Jakarta
|
||||||
const now = new Date();
|
const now = getJakartaDate();
|
||||||
const selectedDateTime = new Date();
|
const selectedDateTime = new Date(now);
|
||||||
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
|
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
|
||||||
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
|
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
|
||||||
|
|
||||||
// Uncomment this for production to prevent booking past times
|
// Uncomment this for production to prevent booking past times
|
||||||
if (selectedDateTime <= now) {
|
if (selectedDateTime <= now) {
|
||||||
alert('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.');
|
showToast('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('show-loading'));
|
||||||
|
|
||||||
// Hitung end time
|
// Hitung end time
|
||||||
const bookingStart = new Date();
|
const bookingStart = new Date();
|
||||||
|
@ -419,18 +609,18 @@ function formatPrice(price) {
|
||||||
|
|
||||||
const endTimeFormatted = ('0' + bookingEnd.getHours()).slice(-2) + ':' + ('0' + bookingEnd.getMinutes()).slice(-2);
|
const endTimeFormatted = ('0' + bookingEnd.getHours()).slice(-2) + ':' + ('0' + bookingEnd.getMinutes()).slice(-2);
|
||||||
|
|
||||||
// PERUBAHAN DI SINI: Gunakan tanggal WIB dengan menambahkan offset +7 jam
|
// Gunakan tanggal Jakarta yang konsisten
|
||||||
const nowUtc = new Date();
|
const today = getJakartaDateString();
|
||||||
const jakartaTime = new Date(nowUtc.getTime() + (7 * 60 * 60 * 1000));
|
|
||||||
const today = jakartaTime.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
const start_time = `${today} ${selectedTime}`;
|
const start_time = `${today} ${selectedTime}`;
|
||||||
const end_time = `${today} ${endTimeFormatted}`;
|
const end_time = `${today} ${endTimeFormatted}`;
|
||||||
|
|
||||||
|
console.log('Booking data:', { start_time, end_time, today });
|
||||||
|
|
||||||
// Track that we're creating a new booking
|
// Track that we're creating a new booking
|
||||||
window.creatingNewBooking = true;
|
window.creatingNewBooking = true;
|
||||||
|
|
||||||
// Kirim ke backend untuk membuat payment intent (tanpa membuat booking dulu)
|
// Kirim ke backend untuk membuat payment intent
|
||||||
fetch('/booking/payment-intent', {
|
fetch('/booking/payment-intent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -446,52 +636,72 @@ function formatPrice(price) {
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return res.json().then(err => {
|
return res.json().then(err => {
|
||||||
throw new Error(err.message || 'Gagal membuat payment intent');
|
throw new Error(err.message || 'Gagal membuat booking');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
|
|
||||||
|
// Cek apakah ini response dari admin direct booking
|
||||||
|
if (data.booking_id && data.message === 'Booking created successfully') {
|
||||||
|
// Admin booking berhasil tanpa payment
|
||||||
|
showToast('Booking berhasil dibuat!', 'success');
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
this.selectedTime = '';
|
||||||
|
this.selectedDuration = '';
|
||||||
|
this.open = false;
|
||||||
|
|
||||||
|
// Refresh halaman atau komponen yang diperlukan
|
||||||
|
this.checkBookedSchedules();
|
||||||
|
|
||||||
|
// Refresh pending bookings jika ada
|
||||||
|
document.dispatchEvent(refreshPendingBookingsEvent);
|
||||||
|
|
||||||
|
return; // Exit early untuk admin booking
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow normal untuk customer (membutuhkan payment)
|
||||||
if (!data.snap_token) {
|
if (!data.snap_token) {
|
||||||
throw new Error('Snap token tidak ditemukan');
|
throw new Error('Snap token tidak ditemukan');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buka Snap Midtrans
|
// Track bahwa kita sedang membuat booking baru
|
||||||
|
window.creatingNewBooking = true;
|
||||||
|
|
||||||
|
// Buka Snap Midtrans untuk customer
|
||||||
window.snap.pay(data.snap_token, {
|
window.snap.pay(data.snap_token, {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
this.createBookingAfterPayment(data.order_id, result);
|
this.createBookingAfterPayment(data.order_id, result);
|
||||||
},
|
},
|
||||||
onPending: (result) => {
|
onPending: (result) => {
|
||||||
alert('Pembayaran pending, silahkan selesaikan pembayaran');
|
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
onError: (result) => {
|
onError: (result) => {
|
||||||
alert('Pembayaran gagal');
|
showToast('Pembayaran gagal', 'error');
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
// Reset the state
|
|
||||||
window.creatingNewBooking = false;
|
window.creatingNewBooking = false;
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
|
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
// Set flag to indicate payment popup was just closed
|
|
||||||
window.justClosedPayment = true;
|
window.justClosedPayment = true;
|
||||||
|
|
||||||
// Only trigger the refresh if we were creating a new booking
|
|
||||||
if (window.creatingNewBooking) {
|
if (window.creatingNewBooking) {
|
||||||
// Reset the flag
|
|
||||||
window.creatingNewBooking = false;
|
window.creatingNewBooking = false;
|
||||||
|
|
||||||
// Dispatch the custom event to refresh pending bookings
|
|
||||||
document.dispatchEvent(refreshPendingBookingsEvent);
|
document.dispatchEvent(refreshPendingBookingsEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Payment intent error:', err);
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
alert('Gagal membuat payment: ' + err.message);
|
console.error('Booking error:', err);
|
||||||
|
showToast('Gagal membuat booking: ' + err.message, 'error');
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
window.creatingNewBooking = false;
|
window.creatingNewBooking = false;
|
||||||
});
|
});
|
||||||
|
@ -499,6 +709,8 @@ function formatPrice(price) {
|
||||||
|
|
||||||
// Fungsi untuk menyimpan booking setelah pembayaran berhasil
|
// Fungsi untuk menyimpan booking setelah pembayaran berhasil
|
||||||
createBookingAfterPayment(orderId, paymentResult) {
|
createBookingAfterPayment(orderId, paymentResult) {
|
||||||
|
window.dispatchEvent(new CustomEvent('show-loading'));
|
||||||
|
|
||||||
fetch('/booking', {
|
fetch('/booking', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -521,7 +733,8 @@ function formatPrice(price) {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
alert('Pembayaran dan booking berhasil!');
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
|
showToast('Pembayaran dan booking berhasil!', 'success');
|
||||||
|
|
||||||
// Reset the flag
|
// Reset the flag
|
||||||
window.creatingNewBooking = false;
|
window.creatingNewBooking = false;
|
||||||
|
@ -530,11 +743,14 @@ function formatPrice(price) {
|
||||||
document.dispatchEvent(new CustomEvent('booking-completed'));
|
document.dispatchEvent(new CustomEvent('booking-completed'));
|
||||||
|
|
||||||
// Redirect to booking history page
|
// Redirect to booking history page
|
||||||
window.location.href = '/booking/history';
|
setTimeout(() => {
|
||||||
|
window.location.href = '/booking/history';
|
||||||
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
window.dispatchEvent(new CustomEvent('hide-loading'));
|
||||||
console.error('Booking error:', err);
|
console.error('Booking error:', err);
|
||||||
alert('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message);
|
showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
window.creatingNewBooking = false;
|
window.creatingNewBooking = false;
|
||||||
});
|
});
|
||||||
|
@ -546,5 +762,9 @@ function formatPrice(price) {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize clock update
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock(); // Initial call
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
|
@ -1,168 +1,200 @@
|
||||||
@extends('layouts.super-admin')
|
@extends('layouts.super-admin')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="min-h-screen bg-gray-50 flex items-center justify-center px-4 sm:px-6 lg:px-8 py-12">
|
||||||
<div class="row justify-content-center">
|
<div class="w-full max-w-2xl">
|
||||||
<div class="col-md-8">
|
<div class="bg-white rounded-xl shadow-2xl overflow-hidden"
|
||||||
<div class="card">
|
style="backdrop-filter: blur(20px); background-color: rgba(255, 255, 255, 0.8);">
|
||||||
<div class="card-header">{{ __('Edit Venue') }}</div>
|
<div class="p-6 sm:p-10">
|
||||||
|
<h2 class="text-center text-4xl font-semibold text-gray-900 mb-8">
|
||||||
|
{{ __('Edit Venue') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div class="card-body">
|
@if ($errors->any())
|
||||||
@if ($errors->any())
|
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded-lg">
|
||||||
<div class="alert alert-danger">
|
<ul class="space-y-1 text-sm text-red-700">
|
||||||
<ul>
|
@foreach ($errors->all() as $error)
|
||||||
@foreach ($errors->all() as $error)
|
<li class="flex items-center">
|
||||||
<li>{{ $error }}</li>
|
<svg class="h-4 w-4 mr-2 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
@endforeach
|
<path fill-rule="evenodd"
|
||||||
</ul>
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-11.293a1 1 0 00-1.414-1.414L10 8.586 7.707 6.293a1 1 0 00-1.414 1.414L8.586 10l-2.293 2.293a1 1 0 101.414 1.414L10 11.414l2.293 2.293a1 1 0 001.414-1.414L11.414 10l2.293-2.293z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
{{ $error }}
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('superadmin.venue.update', $venue->id) }}"
|
||||||
|
enctype="multipart/form-data" x-data="venueForm()" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
|
{{-- Nama Venue --}}
|
||||||
|
<div class="col-span-2 md:col-span-1">
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Nama Venue') }}
|
||||||
|
</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ old('name', $venue->name) }}" required
|
||||||
|
autocomplete="name" autofocus
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
|
||||||
|
placeholder="Masukkan nama venue">
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
|
||||||
<form method="POST" action="{{ route('superadmin.venue.update', $venue->id) }}"
|
{{-- Nomor Telepon --}}
|
||||||
enctype="multipart/form-data">
|
<div class="col-span-2 md:col-span-1">
|
||||||
@csrf
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@method('PUT')
|
{{ __('Nomor Telepon') }}
|
||||||
|
</label>
|
||||||
|
<input type="tel" id="phone" name="phone" value="{{ old('phone', $venue->phone) }}" required
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
|
||||||
|
placeholder="Masukkan nomor telepon">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
{{-- Alamat --}}
|
||||||
<label for="name"
|
<div>
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Nama Venue') }}</label>
|
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<div class="col-md-6">
|
{{ __('Alamat') }}
|
||||||
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror"
|
</label>
|
||||||
name="name" value="{{ old('name', $venue->name) }}" required autocomplete="name"
|
<textarea id="address" name="address" required rows="3"
|
||||||
autofocus>
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
|
||||||
@error('name')
|
placeholder="Masukkan alamat lengkap venue">{{ old('address', $venue->address) }}</textarea>
|
||||||
<span class="invalid-feedback" role="alert">
|
</div>
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
{{-- Deskripsi --}}
|
||||||
@enderror
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Deskripsi') }}
|
||||||
|
</label>
|
||||||
|
<textarea id="description" name="description" rows="4" required
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
|
||||||
|
placeholder="Berikan deskripsi venue">{{ old('description', $venue->description) }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Jam Operasional --}}
|
||||||
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="open_time" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Jam Buka') }}
|
||||||
|
</label>
|
||||||
|
<input type="time" id="open_time" name="open_time"
|
||||||
|
value="{{ old('open_time', date('H:i', strtotime($venue->open_time))) }}" required
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="close_time" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Jam Tutup') }}
|
||||||
|
</label>
|
||||||
|
<input type="time" id="close_time" name="close_time"
|
||||||
|
value="{{ old('close_time', date('H:i', strtotime($venue->close_time))) }}" required
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Status --}}
|
||||||
|
<div>
|
||||||
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Status') }}
|
||||||
|
</label>
|
||||||
|
<select id="status" name="status" required
|
||||||
|
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
|
||||||
|
<option value="active" {{ old('status', $venue->status) == 'active' ? 'selected' : '' }}>
|
||||||
|
{{ __('Aktif') }}
|
||||||
|
</option>
|
||||||
|
<option value="inactive" {{ old('status', $venue->status) == 'inactive' ? 'selected' : '' }}>
|
||||||
|
{{ __('Tidak Aktif') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Upload Gambar --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{{ __('Gambar Venue') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{{-- Preview gambar existing --}}
|
||||||
|
@if($venue->image)
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm text-gray-600 mb-2">{{ __('Gambar saat ini:') }}</div>
|
||||||
|
<img src="{{ Storage::url($venue->image) }}" alt="{{ $venue->name }}"
|
||||||
|
class="h-32 w-auto object-cover rounded-lg border border-gray-200 shadow-sm">
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div x-ref="dropzone" @dragover.prevent="dragover = true" @dragleave.prevent="dragover = false"
|
||||||
|
@drop.prevent="handleDrop($event)"
|
||||||
|
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg transition duration-300 ease-in-out"
|
||||||
|
:class="dragover ? 'border-blue-500 bg-blue-50' : 'hover:border-blue-500'">
|
||||||
|
<div class="space-y-1 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none"
|
||||||
|
viewBox="0 0 48 48" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<div class="flex text-sm text-gray-600 justify-center">
|
||||||
|
<label for="image"
|
||||||
|
class="relative cursor-pointer rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500">
|
||||||
|
<span>{{ __('Unggah file baru') }}</span>
|
||||||
|
<input id="image" name="image" type="file" class="sr-only" accept="image/*"
|
||||||
|
@change="handleFileSelect($event)">
|
||||||
|
</label>
|
||||||
|
<p class="pl-1">{{ __('atau seret dan lepas') }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ __('PNG, JPG, GIF hingga 2MB') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ __('Biarkan kosong jika tidak ingin mengubah gambar') }}
|
||||||
|
</p>
|
||||||
|
<p x-text="fileName" class="text-sm text-gray-600 mt-2"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
{{-- Tombol Aksi --}}
|
||||||
<label for="address"
|
<div class="flex justify-end space-x-4 pt-6">
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Alamat') }}</label>
|
<a href="{{ route('superadmin.venue.index') }}"
|
||||||
<div class="col-md-6">
|
class="px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium transition duration-300 ease-in-out">
|
||||||
<textarea id="address" class="form-control @error('address') is-invalid @enderror"
|
{{ __('Batal') }}
|
||||||
name="address" required>{{ old('address', $venue->address) }}</textarea>
|
</a>
|
||||||
@error('address')
|
<button type="submit"
|
||||||
<span class="invalid-feedback" role="alert">
|
class="px-6 py-3 bg-blue-500 text-white rounded-lg font-medium hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-300 ease-in-out">
|
||||||
<strong>{{ $message }}</strong>
|
{{ __('Perbarui') }}
|
||||||
</span>
|
</button>
|
||||||
@enderror
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="phone"
|
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Nomor Telepon') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<input id="phone" type="text" class="form-control @error('phone') is-invalid @enderror"
|
|
||||||
name="phone" value="{{ old('phone', $venue->phone) }}" required>
|
|
||||||
@error('phone')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="description"
|
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Deskripsi') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<textarea id="description"
|
|
||||||
class="form-control @error('description') is-invalid @enderror" name="description"
|
|
||||||
rows="4" required>{{ old('description', $venue->description) }}</textarea>
|
|
||||||
@error('description')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="open_time"
|
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Jam Buka') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<input id="open_time" type="time"
|
|
||||||
class="form-control @error('open_time') is-invalid @enderror" name="open_time"
|
|
||||||
value="{{ old('open_time', $venue->open_time_formatted) }}" required>
|
|
||||||
@error('open_time')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="close_time"
|
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Jam Tutup') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<input id="close_time" type="time"
|
|
||||||
class="form-control @error('close_time') is-invalid @enderror" name="close_time"
|
|
||||||
value="{{ old('close_time', $venue->close_time_formatted) }}" required>
|
|
||||||
@error('close_time')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="image"
|
|
||||||
class="col-md-4 col-form-label text-md-right">{{ __('Gambar Venue') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
@if($venue->image)
|
|
||||||
<div class="mb-2">
|
|
||||||
<img src="{{ $venue->image_url }}" alt="{{ $venue->name }}" class="img-thumbnail"
|
|
||||||
style="max-height: 150px;">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<input id="image" type="file" class="form-control @error('image') is-invalid @enderror"
|
|
||||||
name="image" accept="image/*">
|
|
||||||
<small class="form-text text-muted">Format: JPG, PNG, GIF. Ukuran maksimal: 2MB. Biarkan
|
|
||||||
kosong jika tidak ingin mengubah gambar.</small>
|
|
||||||
@error('image')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-3">
|
|
||||||
<label for="status" class="col-md-4 col-form-label text-md-right">{{ __('Status') }}</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<select id="status" class="form-control @error('status') is-invalid @enderror"
|
|
||||||
name="status" required>
|
|
||||||
<option value="active" {{ old('status', $venue->status) == 'active' ? 'selected' : '' }}>Aktif</option>
|
|
||||||
<option value="inactive" {{ old('status', $venue->status) == 'inactive' ? 'selected' : '' }}>Tidak Aktif</option>
|
|
||||||
</select>
|
|
||||||
@error('status')
|
|
||||||
<span class="invalid-feedback" role="alert">
|
|
||||||
<strong>{{ $message }}</strong>
|
|
||||||
</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mb-0">
|
|
||||||
<div class="col-md-6 offset-md-4">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{{ __('Perbarui') }}
|
|
||||||
</button>
|
|
||||||
<a href="{{ route('superadmin.venue.index') }}" class="btn btn-secondary">
|
|
||||||
{{ __('Batal') }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||||
|
<script>
|
||||||
|
function venueForm() {
|
||||||
|
return {
|
||||||
|
dragover: false,
|
||||||
|
fileName: '',
|
||||||
|
handleFileSelect(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
this.fileName = file ? file.name : '';
|
||||||
|
},
|
||||||
|
handleDrop(event) {
|
||||||
|
this.dragover = false;
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
if (file) {
|
||||||
|
document.getElementById('image').files = event.dataTransfer.files;
|
||||||
|
this.fileName = file.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@endsection
|
@endsection
|
|
@ -68,15 +68,41 @@ class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 tex
|
||||||
@forelse($venues as $venue)
|
@forelse($venues as $venue)
|
||||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="{{ asset('storage/' . ($venue->image ?? 'images/venue-placeholder.jpg')) }}"
|
@php
|
||||||
alt="{{ $venue->name }}" class="w-full h-48 object-cover">
|
// Tentukan path gambar yang akan ditampilkan
|
||||||
|
$imagePath = null;
|
||||||
|
|
||||||
|
if ($venue->image) {
|
||||||
|
// Cek apakah file gambar ada di storage
|
||||||
|
if (Storage::disk('public')->exists($venue->image)) {
|
||||||
|
$imagePath = asset('storage/' . $venue->image);
|
||||||
|
}
|
||||||
|
// Cek apakah file gambar ada di public folder
|
||||||
|
elseif (file_exists(public_path($venue->image))) {
|
||||||
|
$imagePath = asset($venue->image);
|
||||||
|
}
|
||||||
|
// Cek jika path sudah lengkap dengan storage/
|
||||||
|
elseif (file_exists(public_path('storage/' . $venue->image))) {
|
||||||
|
$imagePath = asset('storage/' . $venue->image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback ke placeholder jika gambar tidak ditemukan
|
||||||
|
if (!$imagePath) {
|
||||||
|
$imagePath = asset('images/venue-placeholder.jpg');
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<img src="{{ $imagePath }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover"
|
||||||
|
onerror="this.src='{{ asset('images/venue-placeholder.jpg') }}'; this.onerror=null;">
|
||||||
|
|
||||||
<div class="absolute top-3 right-3 flex gap-2">
|
<div class="absolute top-3 right-3 flex gap-2">
|
||||||
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
|
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
|
||||||
class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" onclick="confirmDelete({{ $venue->id }})"
|
<button type="button" onclick="confirmDelete({{ $venue->id }})"
|
||||||
class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
|
class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors duration-200">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,25 +110,39 @@ class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<i class="fas fa-map text-gray-500 mr-2"></i>
|
<i class="fas fa-map-marker-alt text-gray-500 mr-2"></i>
|
||||||
<span class="text-gray-600 truncate">{{ $venue->address }}</span>
|
<span class="text-gray-600 truncate" title="{{ $venue->address }}">{{ $venue->address }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<i class="fas fa-phone text-gray-500 mr-2"></i>
|
<i class="fas fa-phone text-gray-500 mr-2"></i>
|
||||||
<span class="text-gray-600">{{ $venue->phone }}</span>
|
<span class="text-gray-600">{{ $venue->phone }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-2">
|
||||||
<i
|
<i class="fas fa-clock text-gray-500 mr-2"></i>
|
||||||
class="fas fa-check-circle {{ $venue->status == 'active' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
|
<span class="text-gray-600">
|
||||||
<span class="text-gray-600">{{ $venue->status == 'active' ? 'Aktif' : 'Tidak Aktif' }}</span>
|
{{ $venue->open_time ?? '00:00' }} - {{ $venue->close_time ?? '23:59' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<i class="fas fa-circle {{ $venue->status == 'active' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
|
||||||
|
<span class="text-sm font-medium {{ $venue->status == 'active' ? 'text-green-700' : 'text-red-700' }}">
|
||||||
|
{{ $venue->status == 'active' ? 'Aktif' : 'Tidak Aktif' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($venue->description)
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="text-gray-600 text-sm line-clamp-2">{{ Str::limit($venue->description, 100) }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="border-t pt-4">
|
<div class="border-t pt-4">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm text-gray-500">
|
||||||
<span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
|
<span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
|
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
|
||||||
class="text-green-600 hover:text-green-800 flex items-center text-sm">
|
class="text-green-600 hover:text-green-800 flex items-center text-sm transition-colors duration-200">
|
||||||
Detail
|
Detail
|
||||||
<i class="fas fa-arrow-right ml-1"></i>
|
<i class="fas fa-arrow-right ml-1"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -111,55 +151,66 @@ class="text-green-600 hover:text-green-800 flex items-center text-sm">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div class="col-span-3 bg-white rounded-lg shadow p-6 text-center">
|
<div class="col-span-3 bg-white rounded-lg shadow p-8 text-center">
|
||||||
<i class="fas fa-building text-gray-300 text-5xl mb-4"></i>
|
<div class="max-w-sm mx-auto">
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-1">Belum ada venue</h3>
|
<i class="fas fa-building text-gray-300 text-6xl mb-4"></i>
|
||||||
<p class="text-gray-500 mb-4">Mulai tambahkan venue baru untuk mengelola bisnis Anda</p>
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">Belum ada venue</h3>
|
||||||
<a href="{{ route('superadmin.venue.create') }}"
|
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik</p>
|
||||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center">
|
<a href="{{ route('superadmin.venue.create') }}"
|
||||||
<i class="fas fa-plus mr-2"></i>
|
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center transition-colors duration-200">
|
||||||
Tambah Venue
|
<i class="fas fa-plus mr-2"></i>
|
||||||
</a>
|
Tambah Venue Pertama
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if($venues->hasPages())
|
@if($venues->hasPages())
|
||||||
<div class="mt-6">
|
<div class="mt-8">
|
||||||
{{ $venues->links() }}
|
<div class="bg-white px-4 py-3 border border-gray-200 rounded-lg">
|
||||||
|
{{ $venues->appends(request()->query())->links() }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Delete Venue Confirmation Modal -->
|
<!-- Delete Venue Confirmation Modal -->
|
||||||
<div id="deleteVenueModal" tabindex="-1" aria-hidden="true"
|
<div id="deleteVenueModal" tabindex="-1" aria-hidden="true"
|
||||||
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
|
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full bg-gray-900 bg-opacity-50">
|
||||||
<div class="relative w-full h-full max-w-md md:h-auto">
|
<div class="relative w-full h-full max-w-md md:h-auto mx-auto flex items-center justify-center min-h-screen">
|
||||||
<div class="relative bg-white rounded-lg shadow">
|
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
<div class="flex items-center justify-between p-4 border-b">
|
<div class="flex items-center justify-between p-5 border-b border-gray-200">
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
Konfirmasi Hapus
|
Konfirmasi Hapus
|
||||||
</h3>
|
</h3>
|
||||||
<button type="button" onclick="closeDeleteModal()"
|
<button type="button" onclick="closeDeleteModal()"
|
||||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center">
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center transition-colors duration-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 text-center">
|
<div class="p-6 text-center">
|
||||||
<i class="fas fa-exclamation-triangle text-5xl text-yellow-400 mb-4"></i>
|
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||||
<h3 class="mb-5 text-lg font-normal text-gray-500">Apakah Anda yakin ingin menghapus venue ini? Semua
|
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
|
||||||
data terkait dengan venue ini akan ikut terhapus.</h3>
|
</div>
|
||||||
<form id="deleteVenueForm" method="POST" action="">
|
<h3 class="mb-2 text-lg font-semibold text-gray-900">Hapus Venue</h3>
|
||||||
|
<p class="mb-6 text-sm text-gray-500">
|
||||||
|
Apakah Anda yakin ingin menghapus venue ini? Semua data terkait dengan venue ini akan ikut terhapus
|
||||||
|
dan tidak dapat dikembalikan.
|
||||||
|
</p>
|
||||||
|
<form id="deleteVenueForm" method="POST" action="" class="space-y-4">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit"
|
<div class="flex justify-center space-x-3">
|
||||||
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
<button type="submit"
|
||||||
Ya, saya yakin
|
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors duration-200">
|
||||||
</button>
|
Ya, Hapus
|
||||||
<button type="button" onclick="closeDeleteModal()"
|
</button>
|
||||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10">
|
<button type="button" onclick="closeDeleteModal()"
|
||||||
Batal
|
class="px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-200">
|
||||||
</button>
|
Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -174,11 +225,13 @@ function confirmDelete(venueId) {
|
||||||
// Show modal
|
// Show modal
|
||||||
const modal = document.getElementById('deleteVenueModal');
|
const modal = document.getElementById('deleteVenueModal');
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
|
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDeleteModal() {
|
function closeDeleteModal() {
|
||||||
const modal = document.getElementById('deleteVenueModal');
|
const modal = document.getElementById('deleteVenueModal');
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
|
document.body.style.overflow = 'auto'; // Restore scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
@ -190,13 +243,27 @@ function closeDeleteModal() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click outside modal closes it
|
// Click outside modal closes it
|
||||||
window.onclick = function (event) {
|
document.getElementById('deleteVenueModal').addEventListener('click', function (event) {
|
||||||
const modal = document.getElementById('deleteVenueModal');
|
if (event.target === this) {
|
||||||
if (event.target === modal) {
|
|
||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Image error handling function
|
||||||
|
function handleImageError(img) {
|
||||||
|
img.src = '{{ asset("images/venue-placeholder.jpg") }}';
|
||||||
|
img.onerror = null; // Prevent infinite loop
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
Loading…
Reference in New Issue