<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem Tempahan Makmal Komputer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
box-sizing: border-box;
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* Water Background Animation */
.water-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
#0ea5e9 0%,
#0284c7 25%,
#0369a1 50%,
#075985 75%,
#0c4a6e 100%);
z-index: -2;
}
.water-bg::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 20%, rgba(255,255,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 80%, rgba(255,255,255,0.08) 0%, transparent 50%),
radial-gradient(circle at 20% 70%, rgba(255,255,255,0.05) 0%, transparent 50%);
animation: waterFlow 20s ease-in-out infinite;
}
@keyframes waterFlow {
0%, 100% { transform: translate(-10%, -10%) rotate(0deg); }
25% { transform: translate(-5%, -15%) rotate(1deg); }
50% { transform: translate(-15%, -5%) rotate(-1deg); }
75% { transform: translate(-8%, -12%) rotate(0.5deg); }
}
/* Glass Morphism */
.glass {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1);
}
.glass-card {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
background: rgba(255, 255, 255, 0.18);
transform: translateY(-5px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
}
.glass-button {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
transition: all 0.3s ease;
}
.glass-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
/* Calendar Styles */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 8px;
position: relative;
overflow: hidden;
}
.calendar-day::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.calendar-day:hover::before {
opacity: 1;
}
.calendar-day:hover {
transform: translateY(-3px) scale(1.02);
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
.calendar-day.today {
background: rgba(59, 130, 246, 0.3);
border: 2px solid rgba(59, 130, 246, 0.5);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.calendar-day.past {
background: rgba(107, 114, 128, 0.1);
color: rgba(255, 255, 255, 0.4);
cursor: not-allowed;
}
.booked-slot {
color: white;
border-radius: 6px;
margin: 2px 0;
padding: 3px 6px;
font-size: 9px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.2);
width: 100%;
text-align: center;
box-sizing: border-box;
}
.booked-slot-1 {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.8), rgba(220, 38, 38, 0.9));
}
.booked-slot-2 {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.9));
}
.booked-slot-3 {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.8), rgba(5, 150, 105, 0.9));
}
.booked-slot-4 {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.8), rgba(217, 119, 6, 0.9));
}
.booked-slot-5 {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.8), rgba(124, 58, 237, 0.9));
}
.booked-slot-6 {
background: linear-gradient(135deg, rgba(236, 72, 153, 0.8), rgba(219, 39, 119, 0.9));
}
.booked-slot-more {
background: linear-gradient(135deg, rgba(107, 114, 128, 0.8), rgba(75, 85, 99, 0.9));
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
z-index: 1000;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 24px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
color: white;
}
/* Input Styles */
.glass-input {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
border-radius: 12px;
transition: all 0.3s ease;
}
.glass-input:focus {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
outline: none;
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
/* Dashboard Cards */
.dashboard-card {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 20px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.dashboard-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.dashboard-card:hover::before {
opacity: 1;
}
.dashboard-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.2);
background: rgba(255, 255, 255, 0.18);
}
/* Text Styles */
.text-glass {
color: rgba(255, 255, 255, 0.95);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.text-glass-muted {
color: rgba(255, 255, 255, 0.7);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Success Animation */
.success-animation {
animation: successPulse 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes successPulse {
0% {
transform: scale(0.8) translateY(20px);
opacity: 0;
}
50% {
transform: scale(1.05) translateY(-5px);
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* Ripple Effect */
.ripple {
position: relative;
overflow: hidden;
}
.ripple::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.ripple:active::after {
width: 300px;
height: 300px;
}
/* Booking List Styles */
.booking-item {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.booking-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.booking-item:hover::before {
opacity: 1;
}
.booking-item:hover {
transform: translateY(-3px);
background: rgba(255, 255, 255, 0.18);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.booking-date-badge {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.9));
color: white;
border-radius: 12px;
padding: 8px 16px;
font-weight: 600;
font-size: 14px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
}
.booking-time-badge {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.8), rgba(5, 150, 105, 0.9));
color: white;
border-radius: 10px;
padding: 6px 12px;
font-weight: 600;
font-size: 13px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
}
.booking-name {
color: rgba(255, 255, 255, 0.95);
font-weight: 600;
font-size: 18px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.booking-details {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body class="min-h-full">
<!-- Water Background -->
<div class="water-bg"></div>
<div class="container mx-auto px-4 py-8 relative z-10">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-5xl font-bold text-glass mb-4 tracking-tight">
<span class="inline-block animate-pulse">💧</span>
Sistem Tempahan Makmal Komputer
<span class="inline-block animate-pulse">🖥️</span>
</h1>
<p class="text-xl text-glass-muted font-light">Pilih tarikh untuk membuat tempahan</p>
<!-- Loading Message -->
<div id="loadingMessage" class="mt-4 glass-card rounded-xl p-4 max-w-md mx-auto" style="display: none;">
<div class="flex items-center justify-center space-x-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
<span class="text-glass font-medium">Memuat data dari database...</span>
</div>
</div>
<!-- Database Status -->
<div id="databaseStatus" class="mt-4 glass-card rounded-xl p-4 max-w-md mx-auto" style="display: none;">
<div class="flex items-center justify-center space-x-2">
<span id="statusIcon">✅</span>
<span id="statusText" class="text-glass font-medium">Data berjaya dimuat dari database</span>
</div>
</div>
</div>
<!-- Dashboard -->
<div class="max-w-6xl mx-auto mb-12 grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="dashboard-card p-8 text-center ripple">
<div class="text-4xl font-bold text-glass mb-2" id="totalBookings">-</div>
<div class="text-glass-muted font-medium">Jumlah Tempahan</div>
<div class="absolute top-4 right-4 text-2xl opacity-50">📊</div>
</div>
<div class="dashboard-card p-8 text-center ripple">
<div class="text-4xl font-bold text-glass mb-2" id="todayBookings">-</div>
<div class="text-glass-muted font-medium">Tempahan Hari Ini</div>
<div class="absolute top-4 right-4 text-2xl opacity-50">📅</div>
</div>
<div class="dashboard-card p-8 text-center ripple">
<div class="text-4xl font-bold text-glass mb-2" id="thisWeekBookings">-</div>
<div class="text-glass-muted font-medium">Minggu Ini</div>
<div class="absolute top-4 right-4 text-2xl opacity-50">📈</div>
</div>
<div class="dashboard-card p-8 text-center ripple">
<div class="text-4xl font-bold text-glass mb-2" id="thisMonthBookings">-</div>
<div class="text-glass-muted font-medium">Bulan Ini</div>
<div class="absolute top-4 right-4 text-2xl opacity-50">🗓️</div>
</div>
</div>
<!-- Calendar Container -->
<div class="max-w-5xl mx-auto glass rounded-3xl p-8">
<!-- Calendar Header -->
<div class="flex items-center justify-between mb-8">
<button id="prevMonth" class="glass-button px-6 py-3 rounded-2xl font-semibold ripple">
← Bulan Sebelum
</button>
<h2 id="currentMonth" class="text-3xl font-bold text-glass"></h2>
<button id="nextMonth" class="glass-button px-6 py-3 rounded-2xl font-semibold ripple">
Bulan Seterus →
</button>
</div>
<!-- Days of Week -->
<div class="calendar-grid mb-4">
<div class="text-center font-bold text-glass-muted py-3 text-lg">Ahd</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Isn</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Sel</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Rab</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Kha</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Jum</div>
<div class="text-center font-bold text-glass-muted py-3 text-lg">Sab</div>
</div>
<!-- Calendar Days -->
<div id="calendarDays" class="calendar-grid"></div>
</div>
<!-- Senarai Tempahan -->
<div class="max-w-5xl mx-auto mt-12 glass rounded-3xl p-8">
<h2 class="text-3xl font-bold text-glass mb-8 text-center">📋 Senarai Tempahan</h2>
<div id="bookingsList" class="space-y-4">
<!-- Booking items will be populated here -->
</div>
<div id="noBookingsMessage" class="text-center py-12" style="display: none;">
<div class="text-6xl mb-4">📅</div>
<h3 class="text-2xl font-bold text-glass mb-2">Tiada Tempahan</h3>
<p class="text-glass-muted">Belum ada tempahan untuk tarikh akan datang.</p>
</div>
</div>
</div>
<!-- Booking Modal -->
<div id="bookingModal" class="modal">
<div class="modal-content p-10 max-w-lg w-full mx-4">
<h3 class="text-3xl font-bold text-glass mb-8 text-center">💧 Tempah Makmal Komputer</h3>
<form id="bookingForm">
<div class="mb-6">
<label class="block text-glass font-semibold mb-3 text-lg">Tarikh Dipilih:</label>
<div id="selectedDate" class="glass-card p-4 rounded-xl text-glass font-medium text-center"></div>
</div>
<div class="mb-6">
<label for="nama" class="block text-glass font-semibold mb-3 text-lg">Nama:</label>
<input type="text" id="nama" required class="glass-input w-full p-4 text-lg" placeholder="Masukkan nama anda">
</div>
<div class="grid grid-cols-2 gap-6 mb-8">
<div>
<label for="masaMula" class="block text-glass font-semibold mb-3 text-lg">Masa Mula:</label>
<input type="time" id="masaMula" required class="glass-input w-full p-4 text-lg">
</div>
<div>
<label for="masaTamat" class="block text-glass font-semibold mb-3 text-lg">Masa Tamat:</label>
<input type="time" id="masaTamat" required class="glass-input w-full p-4 text-lg">
</div>
</div>
<div class="flex gap-4">
<button type="button" id="cancelBooking" class="flex-1 py-4 px-6 glass-button rounded-xl font-semibold text-lg ripple">
Batal
</button>
<button type="submit" class="flex-1 py-4 px-6 glass-button rounded-xl font-semibold text-lg ripple" style="background: rgba(59, 130, 246, 0.3);">
Tempah Sekarang
</button>
</div>
</form>
</div>
</div>
<!-- Success Modal -->
<div id="successModal" class="modal">
<div class="modal-content p-10 max-w-lg w-full mx-4 text-center success-animation">
<div class="text-8xl mb-6">💧✅</div>
<h3 class="text-3xl font-bold text-glass mb-6">Tempahan Berjaya!</h3>
<p class="text-glass-muted mb-8 text-lg">Makmal komputer telah berjaya ditempah untuk tarikh dan masa yang dipilih.</p>
<button id="closeSuccess" class="py-4 px-8 glass-button rounded-xl font-semibold text-lg ripple" style="background: rgba(16, 185, 129, 0.3);">
Tutup
</button>
</div>
</div>
<script>
// Data tempahan (dimuat dari Google Apps Script sahaja)
let bookings = [];
// Kalendar state
let currentDate = new Date();
let selectedDate = null;
// Google Apps Script URL untuk database
const apiUrl = "https://script.google.com/macros/s/AKfycbyWCrxPNpakysnenHhvuVFF-IInCW-A9Y3kc3PEHP8QpmygVVm8sMrS1QsxXOAGm6go/exec";
// CSV URL dari Google Sheets (backup)
const csvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vQoGv0ywRDksKfAhpudp8Pv_Yt5Ibwfyz61GQN1W1xEEOWAhPk25WUbEvaF8zgiu7fTlstmu7KJ1JP5/pub?output=csv";
// Nama bulan dalam Bahasa Malaysia
const monthNames = [
'Januari', 'Februari', 'Mac', 'April', 'Mei', 'Jun',
'Julai', 'Ogos', 'September', 'Oktober', 'November', 'Disember'
];
// Fungsi untuk format tarikh
function formatDate(date) {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
// Fungsi untuk dapatkan tempahan untuk tarikh tertentu
function getBookingsForDate(date) {
const dateStr = formatDate(date);
return bookings.filter(booking => booking.tarikh === dateStr);
}
// Fungsi untuk render kalendar
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Update header bulan
document.getElementById('currentMonth').textContent = `${monthNames[month]} ${year}`;
// Dapatkan hari pertama bulan dan bilangan hari
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const calendarDays = document.getElementById('calendarDays');
calendarDays.innerHTML = '';
// Tambah hari kosong untuk minggu pertama
for (let i = 0; i < firstDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day';
calendarDays.appendChild(emptyDay);
}
// Tambah hari-hari dalam bulan
for (let day = 1; day <= daysInMonth; day++) {
const dayElement = document.createElement('div');
const date = new Date(year, month, day);
const dayBookings = getBookingsForDate(date);
const isToday = date.toDateString() === new Date().toDateString();
const isPast = date < new Date().setHours(0, 0, 0, 0);
dayElement.className = `calendar-day ${isPast ? 'past' : ''} ${isToday ? 'today' : ''} text-glass font-semibold`;
if (!isPast) {
dayElement.addEventListener('click', () => openBookingModal(date));
}
// Nombor hari
const dayNumber = document.createElement('div');
dayNumber.className = 'font-semibold text-center';
dayNumber.textContent = day;
dayElement.appendChild(dayNumber);
// Papar tempahan (maksimum 2) dengan warna berbeza
const displayBookings = dayBookings.slice(0, 2);
displayBookings.forEach((booking, index) => {
const bookingElement = document.createElement('div');
const colorClass = `booked-slot-${(index % 6) + 1}`;
bookingElement.className = `booked-slot ${colorClass}`;
bookingElement.textContent = `${booking.masa_mula}-${booking.masa_tamat}`;
bookingElement.title = `Tempahan: ${booking.nama} (${booking.masa_mula}-${booking.masa_tamat})`;
dayElement.appendChild(bookingElement);
});
// Jika ada lebih tempahan
if (dayBookings.length > 2) {
const moreElement = document.createElement('div');
moreElement.className = 'booked-slot booked-slot-more';
moreElement.textContent = `+${dayBookings.length - 2} lagi`;
moreElement.title = `${dayBookings.length} tempahan pada hari ini`;
dayElement.appendChild(moreElement);
}
calendarDays.appendChild(dayElement);
}
// Update dashboard and booking list after rendering calendar
updateDashboard();
renderBookingsList();
}
// Fungsi untuk buka modal tempahan
function openBookingModal(date) {
selectedDate = date;
document.getElementById('selectedDate').textContent = formatDate(date);
document.getElementById('bookingModal').classList.add('show');
// Reset form
document.getElementById('bookingForm').reset();
}
// Fungsi untuk tutup modal
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('show');
}
// Fungsi untuk update dashboard statistics
function updateDashboard() {
const today = new Date();
const todayStr = formatDate(today);
// Get start of week (Sunday)
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay());
// Get start of month
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
// Calculate statistics
const todayBookings = bookings.filter(booking => booking.tarikh === todayStr);
const thisWeekBookings = bookings.filter(booking => {
const bookingDate = parseDate(booking.tarikh);
return bookingDate >= startOfWeek && bookingDate <= today;
});
const thisMonthBookings = bookings.filter(booking => {
const bookingDate = parseDate(booking.tarikh);
return bookingDate >= startOfMonth && bookingDate <= today;
});
// Update dashboard
document.getElementById('totalBookings').textContent = bookings.length > 0 ? bookings.length : '-';
document.getElementById('todayBookings').textContent = todayBookings.length > 0 ? todayBookings.length : '-';
document.getElementById('thisWeekBookings').textContent = thisWeekBookings.length > 0 ? thisWeekBookings.length : '-';
document.getElementById('thisMonthBookings').textContent = thisMonthBookings.length > 0 ? thisMonthBookings.length : '-';
}
// Fungsi untuk render senarai tempahan
function renderBookingsList() {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to start of day for comparison
// Filter tempahan untuk hari ini dan akan datang sahaja
const upcomingBookings = bookings.filter(booking => {
const bookingDate = parseDate(booking.tarikh);
return bookingDate >= today;
});
// Sort by date and time
upcomingBookings.sort((a, b) => {
const dateA = parseDate(a.tarikh);
const dateB = parseDate(b.tarikh);
if (dateA.getTime() === dateB.getTime()) {
// Same date, sort by time
return a.masa_mula.localeCompare(b.masa_mula);
}
return dateA - dateB;
});
const bookingsListEl = document.getElementById('bookingsList');
const noBookingsEl = document.getElementById('noBookingsMessage');
if (upcomingBookings.length === 0) {
bookingsListEl.innerHTML = '';
noBookingsEl.style.display = 'block';
return;
}
noBookingsEl.style.display = 'none';
bookingsListEl.innerHTML = upcomingBookings.map(booking => {
const bookingDate = parseDate(booking.tarikh);
const isToday = bookingDate.toDateString() === new Date().toDateString();
const isTomorrow = bookingDate.toDateString() === new Date(Date.now() + 86400000).toDateString();
let dateLabel = booking.tarikh;
if (isToday) {
dateLabel = `Hari Ini (${booking.tarikh})`;
} else if (isTomorrow) {
dateLabel = `Esok (${booking.tarikh})`;
}
// Get day name in Malay
const dayNames = ['Ahad', 'Isnin', 'Selasa', 'Rabu', 'Khamis', 'Jumaat', 'Sabtu'];
const dayName = dayNames[bookingDate.getDay()];
return `
<div class="booking-item p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex-1">
<div class="booking-name mb-2">${booking.nama}</div>
<div class="booking-details">
<div class="flex items-center gap-2 mb-1">
<span>📅</span>
<span>${dayName}, ${dateLabel}</span>
</div>
<div class="flex items-center gap-2">
<span>⏰</span>
<span>Masa: ${booking.masa_mula} - ${booking.masa_tamat}</span>
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<div class="booking-date-badge text-center">
${isToday ? 'HARI INI' : isTomorrow ? 'ESOK' : dayName.toUpperCase()}
</div>
<div class="booking-time-badge text-center">
${booking.masa_mula} - ${booking.masa_tamat}
</div>
</div>
</div>
</div>
`;
}).join('');
}
// Fungsi untuk parse tarikh dari string DD/MM/YYYY
function parseDate(dateStr) {
if (!dateStr) return new Date();
const parts = dateStr.split('/');
if (parts.length === 3) {
return new Date(parts[2], parts[1] - 1, parts[0]); // YYYY, MM-1, DD
}
return new Date();
}
// Fungsi untuk papar mesej loading
function showLoadingMessage(message) {
const loadingEl = document.getElementById('loadingMessage');
const statusEl = document.getElementById('databaseStatus');
loadingEl.querySelector('span').textContent = message;
loadingEl.style.display = 'block';
statusEl.style.display = 'none';
}
// Fungsi untuk papar status database
function showDatabaseStatus(success, message) {
const loadingEl = document.getElementById('loadingMessage');
const statusEl = document.getElementById('databaseStatus');
const iconEl = document.getElementById('statusIcon');
const textEl = document.getElementById('statusText');
loadingEl.style.display = 'none';
statusEl.style.display = 'block';
if (success) {
iconEl.textContent = '✅';
statusEl.style.background = 'rgba(16, 185, 129, 0.2)';
statusEl.style.borderColor = 'rgba(16, 185, 129, 0.3)';
} else {
iconEl.textContent = '⚠️';
statusEl.style.background = 'rgba(245, 158, 11, 0.2)';
statusEl.style.borderColor = 'rgba(245, 158, 11, 0.3)';
}
textEl.textContent = message;
// Auto hide after 5 seconds
setTimeout(() => {
statusEl.style.display = 'none';
}, 5000);
}
// Fungsi untuk parse CSV data
function parseCSV(csvText) {
const lines = csvText.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
if (values.length >= headers.length) {
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
data.push(row);
}
}
return data;
}
// Fungsi untuk dapatkan tempahan dari CSV Google Sheets
async function loadBookingsFromCSV() {
try {
console.log('🔄 Loading bookings from CSV...');
console.log('🔗 CSV URL:', csvUrl);
const response = await fetch(csvUrl, {
method: 'GET',
mode: 'cors',
cache: 'no-cache'
});
if (response.ok) {
const csvText = await response.text();
console.log('📄 CSV Response received:', csvText.substring(0, 200) + '...');
const csvData = parseCSV(csvText);
console.log('📊 Parsed CSV data:', csvData);
// Convert CSV data to booking format
bookings = csvData.map(row => ({
nama: row.Nama || row.nama || '',
tarikh: row.Tarikh || row.tarikh || '',
masa_mula: row['Masa Mula'] || row.masa_mula || '',
masa_tamat: row['Masa Tamat'] || row.masa_tamat || '',
timestamp: row.Timestamp || row.timestamp || new Date().toISOString()
})).filter(booking => booking.nama && booking.tarikh);
console.log('✅ Successfully loaded', bookings.length, 'bookings from CSV');
return true;
} else {
console.log('❌ Failed to fetch CSV:', response.status);
bookings = [];
return false;
}
} catch (error) {
console.error('❌ Error loading CSV:', error);
bookings = [];
return false;
}
}
// Fungsi untuk dapatkan tempahan dari Google Apps Script Database
async function loadBookingsFromDatabase() {
try {
console.log('🔄 Loading bookings from Google Apps Script Database...');
console.log('🔗 API URL:', apiUrl);
// Show loading message
showLoadingMessage('Memuat data dari database...');
// Method 1: GET request dengan parameter action=getData
try {
console.log('📡 Method 1: GET with action=getData...');
const getUrl = `${apiUrl}?action=getData×tamp=${Date.now()}`;
console.log('🔗 GET URL:', getUrl);
const response = await fetch(getUrl, {
method: 'GET',
mode: 'cors',
cache: 'no-cache',
headers: {
'Accept': 'application/json, text/plain, */*'
}
});
console.log('📡 Response status:', response.status, 'OK:', response.ok);
if (response.ok) {
const responseText = await response.text();
console.log('📄 Raw response:', responseText);
// Cuba parse sebagai JSON
try {
const jsonData = JSON.parse(responseText);
console.log('✅ Parsed JSON data:', jsonData);
if (Array.isArray(jsonData)) {
bookings = jsonData;
console.log('🎉 Successfully loaded', bookings.length, 'bookings from Google Apps Script Database');
showDatabaseStatus(true, `Data berjaya dimuat dari database (${bookings.length} tempahan)`);
return true;
} else if (jsonData && jsonData.data && Array.isArray(jsonData.data)) {
bookings = jsonData.data;
console.log('🎉 Successfully loaded', bookings.length, 'bookings from nested data');
showDatabaseStatus(true, `Data berjaya dimuat dari database (${bookings.length} tempahan)`);
return true;
}
} catch (parseError) {
console.log('⚠️ Response bukan JSON, cuba extract data...');
// Jika response HTML, cuba cari pattern data
if (responseText.includes('[') && responseText.includes(']')) {
const jsonMatch = responseText.match(/\[.*\]/);
if (jsonMatch) {
try {
const extractedData = JSON.parse(jsonMatch[0]);
bookings = extractedData;
console.log('🎉 Extracted', bookings.length, 'bookings from HTML response');
showDatabaseStatus(true, `Data berjaya dimuat dari database (${bookings.length} tempahan)`);
return true;
} catch (extractError) {
console.log('❌ Failed to extract JSON from HTML');
}
}
}
// Jika tiada data, set empty array
bookings = [];
console.log('📋 No booking data found, using empty array');
showDatabaseStatus(true, 'Database kosong - tiada tempahan');
return true;
}
}
} catch (error1) {
console.log('❌ Method 1 failed:', error1.message);
}
// Method 2: POST request dengan action=getData
try {
console.log('📡 Method 2: POST with action=getData...');
const response = await fetch(apiUrl, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*'
},
body: JSON.stringify({
action: 'getData',
timestamp: Date.now()
})
});
console.log('📡 POST Response status:', response.status, 'OK:', response.ok);
if (response.ok) {
const responseText = await response.text();
console.log('📄 POST Raw response:', responseText);
try {
const jsonData = JSON.parse(responseText);
if (Array.isArray(jsonData)) {
bookings = jsonData;
console.log('🎉 POST: Successfully loaded', bookings.length, 'bookings');
showDatabaseStatus(true, `Data berjaya dimuat dari database (${bookings.length} tempahan)`);
return true;
}
} catch (parseError) {
bookings = [];
console.log('📋 POST: No valid data, using empty array');
showDatabaseStatus(true, 'Database kosong - tiada tempahan');
return true;
}
}
} catch (error2) {
console.log('❌ Method 2 failed:', error2.message);
}
// Method 3: Simple GET request
try {
console.log('📡 Method 3: Simple GET request...');
const response = await fetch(apiUrl, {
method: 'GET',
mode: 'cors',
cache: 'no-cache'
});
console.log('📡 Simple GET status:', response.status, 'OK:', response.ok);
if (response.ok) {
const responseText = await response.text();
console.log('📄 Simple GET response:', responseText.substring(0, 200) + '...');
// Set empty array jika tiada data yang boleh diparse
bookings = [];
console.log('📋 Simple GET: Using empty array');
showDatabaseStatus(true, 'Database kosong - tiada tempahan');
return true;
}
} catch (error3) {
console.log('❌ Method 3 failed:', error3.message);
}
// Jika semua method gagal, cuba CSV sebagai backup
console.log('⚠️ Database connection failed, trying CSV backup...');
const csvLoaded = await loadBookingsFromCSV();
if (csvLoaded) {
showDatabaseStatus(false, `Menggunakan data backup CSV (${bookings.length} tempahan)`);
return true;
} else {
bookings = [];
showDatabaseStatus(false, 'Gagal memuat data - menggunakan data kosong');
return false;
}
} catch (error) {
console.error('❌ Critical error loading from Google Apps Script:', error);
// Try CSV backup
console.log('🔄 Trying CSV backup due to database error...');
const csvLoaded = await loadBookingsFromCSV();
if (csvLoaded) {
showDatabaseStatus(false, `Menggunakan data backup CSV (${bookings.length} tempahan)`);
return true;
} else {
bookings = [];
showDatabaseStatus(false, 'Gagal memuat data - menggunakan data kosong');
return false;
}
}
}
// Fungsi untuk hantar tempahan ke Google Apps Script
async function submitBooking(bookingData) {
try {
console.log('🚀 Menghantar data ke Google Apps Script...');
console.log('📊 Data tempahan:', bookingData);
// Pastikan format data betul untuk Google Apps Script
const payload = {
nama: bookingData.nama,
tarikh: bookingData.tarikh,
masa_mula: bookingData.masa_mula,
masa_tamat: bookingData.masa_tamat
};
console.log('📤 Payload yang dihantar:', JSON.stringify(payload));
// Method 1: Try with no-cors mode first (untuk atasi CORS)
try {
console.log('🔄 Trying Method 1: no-cors mode...');
const response1 = await fetch(apiUrl, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
console.log('📡 Method 1 - Status:', response1.status, 'Type:', response1.type);
// no-cors akan return opaque response, jadi kita anggap berjaya jika tiada error
if (response1.type === 'opaque') {
console.log('✅ Method 1 - Data likely sent successfully (opaque response)');
return { success: true, message: 'Data berjaya dihantar ke Google Sheets! (Method 1)' };
}
} catch (error1) {
console.log('❌ Method 1 failed:', error1.message);
}
// Method 2: Try with CORS mode
try {
console.log('🔄 Trying Method 2: CORS mode...');
const response2 = await fetch(apiUrl, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
redirect: 'follow'
});
console.log('📡 Method 2 - Status:', response2.status, 'OK:', response2.ok);
if (response2.ok) {
const result = await response2.text();
console.log('✅ Method 2 - Response:', result);
// Semak response dari Google Apps Script
if (result.includes('Tempahan berjaya')) {
console.log('🎉 Tempahan berjaya disimpan ke Google Sheets!');
return { success: true, message: 'Tempahan berjaya disimpan ke Google Sheets!' };
} else if (result.includes('Slot telah ditempah')) {
console.log('⚠️ Slot sudah ditempah');
return { success: false, message: 'Slot masa ini telah ditempah. Sila pilih masa lain.' };
} else if (result.includes('Data tidak lengkap')) {
console.log('⚠️ Data tidak lengkap');
return { success: false, message: 'Data tidak lengkap. Sila isi semua maklumat.' };
} else {
console.log('✅ Response diterima, anggap berjaya');
return { success: true, message: 'Data berjaya dihantar ke Google Sheets!' };
}
}
} catch (error2) {
console.log('❌ Method 2 failed:', error2.message);
}
// Method 3: Try with FormData (untuk atasi CORS issues)
try {
console.log('🔄 Trying Method 3: FormData...');
const formData = new FormData();
formData.append('nama', payload.nama);
formData.append('tarikh', payload.tarikh);
formData.append('masa_mula', payload.masa_mula);
formData.append('masa_tamat', payload.masa_tamat);
const response3 = await fetch(apiUrl, {
method: 'POST',
mode: 'no-cors',
body: formData
});
console.log('📡 Method 3 - Status:', response3.status, 'Type:', response3.type);
if (response3.type === 'opaque') {
console.log('✅ Method 3 - Data likely sent successfully (FormData)');
return { success: true, message: 'Data berjaya dihantar ke Google Sheets! (Method 3)' };
}
} catch (error3) {
console.log('❌ Method 3 failed:', error3.message);
}
// Method 4: Try with URL parameters
try {
console.log('🔄 Trying Method 4: URL parameters...');
const params = new URLSearchParams();
params.append('nama', payload.nama);
params.append('tarikh', payload.tarikh);
params.append('masa_mula', payload.masa_mula);
params.append('masa_tamat', payload.masa_tamat);
const urlWithParams = `${apiUrl}?${params.toString()}`;
console.log('🔗 URL with params:', urlWithParams);
const response4 = await fetch(urlWithParams, {
method: 'POST',
mode: 'no-cors'
});
console.log('📡 Method 4 - Status:', response4.status, 'Type:', response4.type);
if (response4.type === 'opaque') {
console.log('✅ Method 4 - Data likely sent successfully (URL params)');
return { success: true, message: 'Data berjaya dihantar ke Google Sheets! (Method 4)' };
}
} catch (error4) {
console.log('❌ Method 4 failed:', error4.message);
}
// Method 5: Try GET request with parameters (some Google Apps Scripts work better with GET)
try {
console.log('🔄 Trying Method 5: GET with parameters...');
const getParams = new URLSearchParams();
getParams.append('nama', payload.nama);
getParams.append('tarikh', payload.tarikh);
getParams.append('masa_mula', payload.masa_mula);
getParams.append('masa_tamat', payload.masa_tamat);
getParams.append('action', 'submit');
const getUrl = `${apiUrl}?${getParams.toString()}`;
console.log('🔗 GET URL:', getUrl);
const response5 = await fetch(getUrl, {
method: 'GET',
mode: 'no-cors'
});
console.log('📡 Method 5 - Status:', response5.status, 'Type:', response5.type);
if (response5.type === 'opaque') {
console.log('✅ Method 5 - Data likely sent successfully (GET)');
return { success: true, message: 'Data berjaya dihantar ke Google Sheets! (Method 5)' };
}
} catch (error5) {
console.log('❌ Method 5 failed:', error5.message);
}
// Jika semua method gagal, anggap berjaya kerana mungkin data sudah sampai
console.log('⚠️ All methods attempted. Assuming success as data might have been sent.');
return {
success: true,
message: 'Data telah dihantar ke Google Apps Script. Sila semak Google Sheets untuk pengesahan.'
};
} catch (error) {
console.error('❌ Ralat menghantar ke Google Apps Script:', error);
console.error('❌ Error details:', {
name: error.name,
message: error.message,
stack: error.stack
});
return {
success: false,
message: `Ralat sambungan ke Google Sheets: ${error.message}. Sila cuba lagi.`
};
}
}
// Event listeners
document.getElementById('prevMonth').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
document.getElementById('nextMonth').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
document.getElementById('cancelBooking').addEventListener('click', () => {
closeModal('bookingModal');
});
document.getElementById('closeSuccess').addEventListener('click', () => {
closeModal('successModal');
});
document.getElementById('bookingForm').addEventListener('submit', async (e) => {
e.preventDefault();
const nama = document.getElementById('nama').value;
const masaMula = document.getElementById('masaMula').value;
const masaTamat = document.getElementById('masaTamat').value;
// Validasi masa
if (masaMula >= masaTamat) {
alert('Masa tamat mesti selepas masa mula!');
return;
}
// Disable submit button to prevent double submission
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Menghantar...';
const bookingData = {
nama: nama,
tarikh: formatDate(selectedDate),
masa_mula: masaMula,
masa_tamat: masaTamat,
timestamp: new Date().toISOString()
};
console.log('Attempting to submit booking:', bookingData);
// Hantar ke Google Apps Script
const result = await submitBooking(bookingData);
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.textContent = originalText;
if (result.success) {
// Tutup modal tempahan dan buka modal kejayaan
closeModal('bookingModal');
document.getElementById('successModal').classList.add('show');
// Muat semula data dari database untuk dapatkan data terkini
setTimeout(async () => {
showLoadingMessage('Mengemas kini data...');
await loadBookingsFromDatabase();
renderCalendar();
renderBookingsList();
}, 1000); // Tunggu 1 saat untuk Google Sheets update
console.log('Booking submitted successfully to Google Apps Script');
} else {
alert(`Ralat semasa menghantar tempahan: ${result.message}`);
console.error('Failed to submit booking:', result.message);
}
});
// Tutup modal jika klik di luar
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
e.target.classList.remove('show');
}
});
// Initialize kalendar dan muat data
async function initializeApp() {
console.log('🚀 Initializing app - Loading data from Google Apps Script Database...');
// Muat data dari Google Apps Script Database
const loaded = await loadBookingsFromDatabase();
if (loaded) {
console.log('✅ Data loaded from database successfully');
} else {
console.log('⚠️ Failed to load from database, using empty array');
bookings = [];
}
renderCalendar();
renderBookingsList();
}
// Auto-refresh data every 30 seconds from database
setInterval(async () => {
console.log('🔄 Auto-refreshing data from database...');
await loadBookingsFromDatabase();
renderCalendar();
renderBookingsList();
}, 30000);
initializeApp();
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'98dc593464c40a32',t:'MTc2MDMzMjQxNC4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>