Checkpoint

This commit is contained in:
Stephen Gesityan 2025-06-04 12:10:22 +07:00
parent 50accc8a00
commit d59f1fb75e
10 changed files with 795 additions and 505 deletions

View File

@ -11,9 +11,9 @@
<a href="{{ route('admin.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left mr-1"></i> Kembali
</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
</a>
</a> --}}
</div>
</div>

View File

@ -15,8 +15,8 @@
</div>
</div>
<!-- Stats Cards - Row 1 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<!-- Stats Cards - Row 1: Revenue and Booking Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- 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="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>
</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="flex justify-between items-start">
<div>
<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="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800">🏆 Top 5 Pelanggan Loyal</h2>
<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"
stroke="currentColor">
<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>
</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 -->
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-amber-500 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-gray-500">Penggunaan Meja</p>
<p class="text-2xl font-bold text-gray-800">
{{ $totalTables > 0 ? round(($usedTables / $totalTables) * 100) : 0 }}%
</p>
@if(!empty($topUsers) && count($topUsers) > 0)
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
@foreach($topUsers->take(5) as $index => $user)
<div
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">
<div class="mb-3">
@if($index === 0)
<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 class="text-amber-500 p-2 bg-amber-50 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" />
</svg>
@else
<div class="text-center py-8">
<div class="text-gray-400 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<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 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>
@endif
</div>
</div>
<!-- Main Performance & Trends Section -->
<!-- Main Performance Section -->
<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="flex justify-between items-center mb-4">
<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>
<div class="flex items-center text-sm text-gray-500">
<span class="mr-2">{{ $booking->table->name }}</span>
<span class="text-xs px-2 py-0.5 rounded-full {{
$booking->status === 'paid' ? 'bg-green-100 text-green-800' :
<span
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')
}}">
}}">
{{ ucfirst($booking->status) }}
</span>
</div>
@ -169,7 +184,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
<div class="h-80" id="weeklyRevenueChart"></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="flex justify-between items-center mb-4">
<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>
</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>
@ -192,7 +199,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Monthly Revenue Chart - ENHANCED TO SHOW 12 MONTHS
// Monthly Revenue Chart
var monthlyRevenueData = @json($lastSixMonthsRevenue);
var monthlyRevenueOptions = {
@ -336,7 +343,7 @@ class="font-semibold text-red-500">{{ $usedTables }}</span></p>
var chart = new ApexCharts(document.querySelector("#weeklyRevenueChart"), options);
chart.render();
// Table Revenue Performance Chart - ENHANCED WITH IMPROVED VISUALS
// Table Revenue Performance Chart
var tableRevenueData = @json($tableRevenue);
// 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);
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>
@endpush

View File

@ -117,7 +117,7 @@ class="inline-flex items-center px-3 py-1.5 border border-transparent rounded-md
</div>
</div>
<!-- Chart: Usage Patterns -->
{{-- <!-- Chart: Usage Patterns -->
<div>
<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">
@ -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>
</div>
</div>
</div>
</div> --}}
</div>
</div>
</div>

View File

@ -127,7 +127,7 @@ class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('
</a>
</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">
Sistem
</div>
@ -153,7 +153,7 @@ class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2 mt
</svg>
<span x-show="sidebarOpen">Pengaturan</span>
</a>
</nav>
</nav> --}}
</div>
<!-- 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 -->
<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">
<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">Profile</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>
<form method="POST" action="{{ route('logout') }}">
@csrf

View File

@ -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-xs text-gray-500">{{ auth()->user()->email ?? 'admin@example.com' }}</p>
</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>
Profile Settings
</a>
<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>
Account Settings
</a>
</a> --}}
<div class="border-t border-gray-100 mt-2 pt-2">
<a href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"

View File

@ -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)
<a href="{{ route('venue', ['venueName' => $venue->name]) }}"
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">
<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>

View File

@ -1,7 +1,11 @@
@extends('layouts.main')
@section('content')
<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">
<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">
@ -26,83 +30,145 @@
<p class="font-medium">{{ $duration }} Jam</p>
</div>
</div>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<div class="mr-3 text-yellow-500">
<i class="fa-solid fa-exclamation-circle text-xl"></i>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<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 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>
<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
<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>
<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>
// 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', () => {
Alpine.data('rescheduleForm', () => ({
tables: @json($venue->tables),
@ -115,22 +181,19 @@ class="text-white px-4 py-2 rounded-lg">
selectedTableId: '',
selectedStartHour: null,
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,
init() {
// Set today as default value
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
this.today = `${year}-${month}-${day}`;
// Set original date and table as default
this.selectedDate = this.originalDate;
this.selectedTableId = this.originalTableId;
// Load schedules for today and selected table
this.checkBookedSchedules();
},
@ -138,7 +201,6 @@ class="text-white px-4 py-2 rounded-lg">
return this.selectedDate &&
this.selectedTableId &&
this.selectedStartHour !== null &&
// Prevent submitting if nothing changed
(this.selectedDate !== this.originalDate ||
this.selectedTableId != this.originalTableId ||
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
@ -173,8 +235,6 @@ class="text-white px-4 py-2 rounded-lg">
}
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 &&
parseInt(this.selectedTableId) === parseInt(this.originalTableId)) {
const originalHour = this.originalStartTime.split(':')[0];
@ -184,7 +244,7 @@ class="text-white px-4 py-2 rounded-lg">
}
} catch (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 endHourInt = hourInt + this.bookingDuration;
// Check if slot end time exceeds midnight
if (endHourInt > 24) return false;
// Check if selected date is today and hour has passed
const selectedDate = new Date(this.selectedDate);
const today = new Date();
const isToday = selectedDate.toDateString() === today.toDateString();
if (isToday) {
const currentHour = today.getHours();
// If the selected hour has already passed today, disable it
if (hourInt <= currentHour) {
return false;
}
}
// Check if this is the original booking's time slot
const isOriginalTimeSlot = this.selectedDate === this.originalDate &&
parseInt(this.selectedTableId) === parseInt(this.originalTableId) &&
hour === this.originalStartTime.split(':')[0];
if (isOriginalTimeSlot) {
// Allow original time slot only if it's not in the past
if (isToday) {
const currentHour = today.getHours();
return hourInt > currentHour;
@ -222,12 +277,10 @@ class="text-white px-4 py-2 rounded-lg">
return true;
}
// Check if any existing booking overlaps with this slot
return !this.bookedSchedules.some(schedule => {
const scheduleStart = parseInt(schedule.start.split(':')[0]);
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
// Check if there's overlap
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
});
},
@ -243,11 +296,9 @@ class="text-white px-4 py-2 rounded-lg">
this.isSubmitting = true;
// Calculate end time based on selected start hour and duration
const startHour = parseInt(this.selectedStartHour);
const endHour = startHour + this.bookingDuration;
// Format date strings for API
const startTime = `${this.selectedDate} ${this.selectedStartHour}: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();
if (result.success) {
alert(result.message);
window.location.href = result.redirect;
showNotification(result.message, 'success');
setTimeout(() => {
window.location.href = result.redirect;
}, 2000);
} else {
alert(result.message || 'Terjadi kesalahan saat memproses reschedule.');
showNotification(result.message || 'Terjadi kesalahan saat memproses reschedule.', 'error');
this.isSubmitting = false;
}
} catch (error) {
console.error('Error submitting reschedule:', error);
alert('Terjadi kesalahan. Silakan coba lagi.');
showNotification('Terjadi kesalahan. Silakan coba lagi.', 'error');
this.isSubmitting = false;
}
}

View File

@ -1,8 +1,25 @@
@extends('layouts.main')
@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="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>
<p class="text-sm text-gray-500">{{ $venue['location'] }}</p>
</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"
data-client-key="{{ config('midtrans.client_key') }}"></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
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 options = { timeZone: 'Asia/Jakarta', hour12: false };
const timeFormatter = new Intl.DateTimeFormat('id-ID', { ...options, hour: '2-digit', minute: '2-digit', second: '2-digit' });
document.getElementById('realTimeClock').textContent = timeFormatter.format(now);
// Buat objek Date dengan timezone Jakarta
const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" }));
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
function formatDateTime(dateTimeStr) {
@ -244,9 +415,13 @@ function formatPrice(price) {
resumeBooking(bookingId) {
this.isLoadingPending = true;
window.dispatchEvent(new CustomEvent('show-loading'));
fetch(`/booking/pending/${bookingId}/resume`)
.then(response => response.json())
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
@ -255,61 +430,68 @@ function formatPrice(price) {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: (result) => {
alert('Pembayaran pending, silahkan selesaikan pembayaran');
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoadingPending = false;
},
onError: (result) => {
alert('Pembayaran gagal');
showToast('Pembayaran gagal', 'error');
this.isLoadingPending = false;
},
onClose: () => {
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoadingPending = false;
}
});
} else {
alert(data.message);
showToast(data.message, 'error');
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
}
})
.catch(error => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Error resuming booking:', error);
alert('Gagal melanjutkan booking');
showToast('Gagal melanjutkan booking', 'error');
this.isLoadingPending = false;
});
},
deletePendingBooking(bookingId) {
if (confirm('Apakah Anda yakin ingin menghapus booking ini?')) {
this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Booking berhasil dihapus');
this.fetchPendingBookings();
} else {
alert(data.message);
showConfirmModal(
'Konfirmasi Hapus',
'Apakah Anda yakin ingin menghapus booking ini?',
() => {
this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
}
this.isLoadingPending = false;
})
.catch(error => {
console.error('Error deleting booking:', error);
alert('Gagal menghapus booking');
this.isLoadingPending = false;
});
}
.then(response => response.json())
.then(data => {
if (data.success) {
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) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: {
@ -332,18 +514,22 @@ function formatPrice(price) {
return res.json();
})
.then(data => {
alert('Pembayaran dan booking berhasil!');
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
// Redirect to booking history
window.location.href = '/booking/history';
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
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;
});
}
@ -375,10 +561,13 @@ function formatPrice(price) {
},
async checkBookedSchedules() {
const today = new Date().toISOString().split('T')[0];
// Gunakan tanggal Jakarta yang konsisten
const today = getJakartaDateString();
try {
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
this.bookedSchedules = await response.json();
console.log('Checking schedules for date:', today, 'Table:', this.tableId);
console.log('Booked schedules:', this.bookedSchedules);
} catch (error) {
console.error('Error checking booked schedules:', error);
}
@ -386,30 +575,31 @@ function formatPrice(price) {
initiateBooking(tableId, tableName) {
if (!this.isLoggedIn) {
alert('Silahkan login terlebih dahulu untuk melakukan booking.');
showToast('Silahkan login terlebih dahulu untuk melakukan booking', 'warning');
return;
}
const selectedTime = this.selectedTime;
const selectedDuration = this.selectedDuration;
if (!selectedTime || !selectedDuration) {
alert('Please select both time and duration');
showToast('Please select both time and duration', 'warning');
return;
}
// Validasi jam
const now = new Date();
const selectedDateTime = new Date();
// Validasi jam menggunakan waktu Jakarta
const now = getJakartaDate();
const selectedDateTime = new Date(now);
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
// Uncomment this for production to prevent booking past times
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;
}
this.isLoading = true;
window.dispatchEvent(new CustomEvent('show-loading'));
// Hitung end time
const bookingStart = new Date();
@ -419,18 +609,18 @@ function formatPrice(price) {
const endTimeFormatted = ('0' + bookingEnd.getHours()).slice(-2) + ':' + ('0' + bookingEnd.getMinutes()).slice(-2);
// PERUBAHAN DI SINI: Gunakan tanggal WIB dengan menambahkan offset +7 jam
const nowUtc = new Date();
const jakartaTime = new Date(nowUtc.getTime() + (7 * 60 * 60 * 1000));
const today = jakartaTime.toISOString().split('T')[0];
// Gunakan tanggal Jakarta yang konsisten
const today = getJakartaDateString();
const start_time = `${today} ${selectedTime}`;
const end_time = `${today} ${endTimeFormatted}`;
console.log('Booking data:', { start_time, end_time, today });
// Track that we're creating a new booking
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', {
method: 'POST',
headers: {
@ -446,52 +636,72 @@ function formatPrice(price) {
.then(res => {
if (!res.ok) {
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();
})
.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) {
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, {
onSuccess: (result) => {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: (result) => {
alert('Pembayaran pending, silahkan selesaikan pembayaran');
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoading = false;
},
onError: (result) => {
alert('Pembayaran gagal');
showToast('Pembayaran gagal', 'error');
this.isLoading = false;
// Reset the state
window.creatingNewBooking = false;
},
onClose: () => {
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoading = false;
// Set flag to indicate payment popup was just closed
window.justClosedPayment = true;
// Only trigger the refresh if we were creating a new booking
if (window.creatingNewBooking) {
// Reset the flag
window.creatingNewBooking = false;
// Dispatch the custom event to refresh pending bookings
document.dispatchEvent(refreshPendingBookingsEvent);
}
}
});
})
.catch(err => {
console.error('Payment intent error:', err);
alert('Gagal membuat payment: ' + err.message);
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err);
showToast('Gagal membuat booking: ' + err.message, 'error');
this.isLoading = false;
window.creatingNewBooking = false;
});
@ -499,6 +709,8 @@ function formatPrice(price) {
// Fungsi untuk menyimpan booking setelah pembayaran berhasil
createBookingAfterPayment(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: {
@ -521,7 +733,8 @@ function formatPrice(price) {
return res.json();
})
.then(data => {
alert('Pembayaran dan booking berhasil!');
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
// Reset the flag
window.creatingNewBooking = false;
@ -530,11 +743,14 @@ function formatPrice(price) {
document.dispatchEvent(new CustomEvent('booking-completed'));
// Redirect to booking history page
window.location.href = '/booking/history';
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
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;
window.creatingNewBooking = false;
});
@ -546,5 +762,9 @@ function formatPrice(price) {
}
}));
});
// Initialize clock update
setInterval(updateClock, 1000);
updateClock(); // Initial call
</script>
@endsection

View File

@ -1,168 +1,200 @@
@extends('layouts.super-admin')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Edit Venue') }}</div>
<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="w-full max-w-2xl">
<div class="bg-white rounded-xl shadow-2xl overflow-hidden"
style="backdrop-filter: blur(20px); background-color: rgba(255, 255, 255, 0.8);">
<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())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@if ($errors->any())
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded-lg">
<ul class="space-y-1 text-sm text-red-700">
@foreach ($errors->all() as $error)
<li class="flex items-center">
<svg class="h-4 w-4 mr-2 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
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>
@endif
<form method="POST" action="{{ route('superadmin.venue.update', $venue->id) }}"
enctype="multipart/form-data">
@csrf
@method('PUT')
{{-- Nomor Telepon --}}
<div class="col-span-2 md:col-span-1">
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
{{ __('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">
<label for="name"
class="col-md-4 col-form-label text-md-right">{{ __('Nama Venue') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror"
name="name" value="{{ old('name', $venue->name) }}" required autocomplete="name"
autofocus>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
{{-- Alamat --}}
<div>
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">
{{ __('Alamat') }}
</label>
<textarea id="address" name="address" required rows="3"
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 alamat lengkap venue">{{ old('address', $venue->address) }}</textarea>
</div>
{{-- Deskripsi --}}
<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 class="form-group row mb-3">
<label for="address"
class="col-md-4 col-form-label text-md-right">{{ __('Alamat') }}</label>
<div class="col-md-6">
<textarea id="address" class="form-control @error('address') is-invalid @enderror"
name="address" required>{{ old('address', $venue->address) }}</textarea>
@error('address')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</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>
{{-- Tombol Aksi --}}
<div class="flex justify-end space-x-4 pt-6">
<a href="{{ route('superadmin.venue.index') }}"
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">
{{ __('Batal') }}
</a>
<button type="submit"
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">
{{ __('Perbarui') }}
</button>
</div>
</form>
</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

View File

@ -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)
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="relative">
<img src="{{ asset('storage/' . ($venue->image ?? 'images/venue-placeholder.jpg')) }}"
alt="{{ $venue->name }}" class="w-full h-48 object-cover">
@php
// 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">
<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>
</a>
<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>
</button>
</div>
@ -84,25 +110,39 @@ class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
<div class="p-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
<div class="flex items-center mb-2">
<i class="fas fa-map text-gray-500 mr-2"></i>
<span class="text-gray-600 truncate">{{ $venue->address }}</span>
<i class="fas fa-map-marker-alt text-gray-500 mr-2"></i>
<span class="text-gray-600 truncate" title="{{ $venue->address }}">{{ $venue->address }}</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-phone text-gray-500 mr-2"></i>
<span class="text-gray-600">{{ $venue->phone }}</span>
</div>
<div class="flex items-center mb-4">
<i
class="fas fa-check-circle {{ $venue->status == 'active' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
<span class="text-gray-600">{{ $venue->status == 'active' ? 'Aktif' : 'Tidak Aktif' }}</span>
<div class="flex items-center mb-2">
<i class="fas fa-clock text-gray-500 mr-2"></i>
<span class="text-gray-600">
{{ $venue->open_time ?? '00:00' }} - {{ $venue->close_time ?? '23:59' }}
</span>
</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="flex justify-between items-center">
<div class="text-sm text-gray-500">
<span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
</div>
<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
<i class="fas fa-arrow-right ml-1"></i>
</a>
@ -111,55 +151,66 @@ class="text-green-600 hover:text-green-800 flex items-center text-sm">
</div>
</div>
@empty
<div class="col-span-3 bg-white rounded-lg shadow p-6 text-center">
<i class="fas fa-building text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-1">Belum ada venue</h3>
<p class="text-gray-500 mb-4">Mulai tambahkan venue baru untuk mengelola bisnis Anda</p>
<a href="{{ route('superadmin.venue.create') }}"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Tambah Venue
</a>
<div class="col-span-3 bg-white rounded-lg shadow p-8 text-center">
<div class="max-w-sm mx-auto">
<i class="fas fa-building text-gray-300 text-6xl mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Belum ada venue</h3>
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik</p>
<a href="{{ route('superadmin.venue.create') }}"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center transition-colors duration-200">
<i class="fas fa-plus mr-2"></i>
Tambah Venue Pertama
</a>
</div>
</div>
@endforelse
</div>
<!-- Pagination -->
@if($venues->hasPages())
<div class="mt-6">
{{ $venues->links() }}
<div class="mt-8">
<div class="bg-white px-4 py-3 border border-gray-200 rounded-lg">
{{ $venues->appends(request()->query())->links() }}
</div>
</div>
@endif
<!-- Delete Venue Confirmation Modal -->
<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">
<div class="relative w-full h-full max-w-md md:h-auto">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-center justify-between p-4 border-b">
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 mx-auto flex items-center justify-center min-h-screen">
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="flex items-center justify-between p-5 border-b border-gray-200">
<h3 class="text-xl font-semibold text-gray-900">
Konfirmasi Hapus
</h3>
<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">
<i class="fas fa-times"></i>
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 w-5 h-5"></i>
</button>
</div>
<div class="p-6 text-center">
<i class="fas fa-exclamation-triangle text-5xl text-yellow-400 mb-4"></i>
<h3 class="mb-5 text-lg font-normal text-gray-500">Apakah Anda yakin ingin menghapus venue ini? Semua
data terkait dengan venue ini akan ikut terhapus.</h3>
<form id="deleteVenueForm" method="POST" action="">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
<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
@method('DELETE')
<button type="submit"
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">
Ya, saya yakin
</button>
<button type="button" onclick="closeDeleteModal()"
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">
Batal
</button>
<div class="flex justify-center space-x-3">
<button type="submit"
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">
Ya, Hapus
</button>
<button type="button" onclick="closeDeleteModal()"
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">
Batal
</button>
</div>
</form>
</div>
</div>
@ -174,11 +225,13 @@ function confirmDelete(venueId) {
// Show modal
const modal = document.getElementById('deleteVenueModal');
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden'; // Prevent background scrolling
}
function closeDeleteModal() {
const modal = document.getElementById('deleteVenueModal');
modal.classList.add('hidden');
document.body.style.overflow = 'auto'; // Restore scrolling
}
document.addEventListener('DOMContentLoaded', function () {
@ -190,13 +243,27 @@ function closeDeleteModal() {
});
// Click outside modal closes it
window.onclick = function (event) {
const modal = document.getElementById('deleteVenueModal');
if (event.target === modal) {
document.getElementById('deleteVenueModal').addEventListener('click', function (event) {
if (event.target === this) {
closeDeleteModal();
}
}
});
});
// Image error handling function
function handleImageError(img) {
img.src = '{{ asset("images/venue-placeholder.jpg") }}';
img.onerror = null; // Prevent infinite loop
}
</script>
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
@endsection