<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ปฏิทินวิชาการ</title>
<!-- Load Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Use Inter font -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<!-- Load Lucide Icons for UI -->
<script type="module" src="https://unpkg.com/lucide@latest"></script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #f4f7f9; }
/* Custom scrollbar for better aesthetics */
.table-container::-webkit-scrollbar { height: 8px; }
.table-container::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
.table-container::-webkit-scrollbar-track { background: #f1f5f9; }
/* Simple transition for interactivity */
.button-primary { transition: all 0.2s; }
.button-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); }
/* Calendar Grid Styles (UPDATED for 7-day calendar view) */
.calendar-grid {
/* Enforce 7 columns for a standard monthly calendar */
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.5rem;
}
.calendar-card {
border: 1px solid #e2e8f0;
background-color: #fff;
padding: 0.5rem; /* Adjusted padding */
border-radius: 0.5rem; /* Adjusted border radius */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
min-height: 100px; /* Ensure minimum height for cell */
}
.calendar-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div id="app" class="min-h-screen p-4 sm:p-8">
<!-- Header Section (Public View: Centered Title, No Admin Button) -->
<header class="flex justify-center items-center bg-white p-6 rounded-xl shadow-lg mb-8">
<h1 class="text-3xl font-extrabold text-blue-800 flex items-center">
<!-- โลโก้ SATRU -->
<img src="https://img5.pic.in.th/file/secure-sv1/LINE_NOTE_251012_1.jpg"
alt="School Logo"
class="w-10 h-10 mr-3 rounded-full border-2 border-indigo-500 p-0.5">
ปฏิทินวิชาการ
</h1>
<!-- REMOVED: Admin Login Button -->
</header>
<!-- Filter Controls (Centered) -->
<div class="flex justify-center mb-6">
<div id="filter-controls" class="flex flex-wrap justify-center items-center gap-4 bg-white p-4 rounded-xl shadow-lg w-full max-w-6xl">
<!-- NEW: Search Input -->
<div class="flex items-center space-x-2 w-full sm:w-1/3 min-w-[200px] order-1 sm:order-none">
<input type="text" id="search-input" placeholder="ค้นหาโครงการ, สถานที่, หรือผู้รับผิดชอบ..."
class="p-2 border border-gray-300 rounded-lg text-sm shadow-inner focus:ring-blue-500 focus:border-blue-500 w-full">
</div>
<!-- View Toggle Buttons -->
<div class="flex space-x-2 order-2 sm:order-none">
<button id="view-table-btn" data-view="table"
class="view-toggle-btn px-4 py-2 text-sm font-semibold rounded-full transition bg-blue-600 text-white shadow-md flex items-center">
<i data-lucide="table" class="w-4 h-4 mr-2"></i> ตาราง
</button>
<button id="view-calendar-btn" data-view="calendar"
class="view-toggle-btn px-4 py-2 text-sm font-medium rounded-full transition text-gray-700 hover:bg-gray-200 flex items-center">
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i> ปฏิทินรายเดือน
</button>
</div>
<!-- Back to Website Link (Distinct Color) -->
<a href="https://satit.vru.ac.th/" target="_blank"
class="px-4 py-2 text-sm font-semibold rounded-full transition bg-red-600 text-white hover:bg-red-700 shadow-lg flex items-center justify-center whitespace-nowrap order-3 sm:order-none">
<i data-lucide="external-link" class="w-4 h-4 mr-2 inline-block"></i>
กลับสู่หน้าเว็บ
</a>
<!-- Filter Dropdowns (Updated) -->
<div class="flex flex-wrap justify-center gap-4 w-full sm:w-auto order-4 sm:order-none">
<!-- NEW: Education Level Dropdown (Replaces buttons) -->
<div class="flex items-center space-x-2">
<label for="category-filter" class="text-sm font-medium text-gray-700 whitespace-nowrap">ระดับการศึกษา:</label>
<select id="category-filter" class="p-2 border border-gray-300 rounded-lg text-sm shadow-inner focus:ring-blue-500 focus:border-blue-500">
<option value="all">ทั้งหมด</option>
<option value="ปฐมวัย">ปฐมวัย</option>
<option value="ประถมศึกษา">ประถมศึกษา</option>
<option value="มัธยมศึกษา">มัธยมศึกษา</option>
<option value="ทั่วไป">ทั่วไป</option>
</select>
</div>
<!-- Academic Year Filter -->
<div class="flex items-center space-x-2">
<label for="year-filter" class="text-sm font-medium text-gray-700 whitespace-nowrap">ปีการศึกษา:</label>
<select id="year-filter" class="p-2 border border-gray-300 rounded-lg text-sm shadow-inner focus:ring-blue-500 focus:border-blue-500">
<option value="all">ทั้งหมด</option>
<!-- Years will be dynamically populated here -->
</select>
</div>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="bg-white p-6 rounded-xl shadow-lg">
<!-- Controls (Items per page) -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6">
<h2 class="text-xl font-bold text-gray-700 mb-4 sm:mb-0" id="view-title">รายการกิจกรรมวิชาการทั้งหมด (มุมมองตาราง)</h2>
<div class="flex items-center space-x-2">
<label for="items-per-page" class="text-gray-600 text-sm">แสดงรายการ:</label>
<select id="items-per-page" class="p-2 border border-gray-300 rounded-lg text-sm shadow-inner focus:ring-blue-500 focus:border-blue-500">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="9999">ทั้งหมด</option>
</select>
</div>
</div>
<!-- Error State -->
<div id="error-message" class="text-center py-10 text-red-500 font-semibold hidden">
เกิดข้อผิดพลาดในการโหลดข้อมูลปฏิทิน
</div>
<!-- Activities Table (Responsive) -->
<div id="calendar-data-view" class="table-container overflow-x-auto rounded-lg border border-gray-200 shadow-inner">
<table class="min-w-full divide-y divide-gray-200">
<!-- THREAD HEADER: Changed to Emerald Green and white text -->
<thead class="bg-emerald-600">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">ลำดับ</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">โครงการ/กิจกรรม</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">สถานที่</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider whitespace-nowrap">วันที่จัดกิจกรรม</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">ระยะเวลา</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider whitespace-nowrap">ปีการศึกษา</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white uppercase tracking-wider whitespace-nowrap">ผู้รับผิดชอบโครงการ</th> <!-- NEW Column -->
<!-- REMOVED: Admin actions header -->
</tr>
</thead>
<tbody id="activities-table-body" class="bg-white divide-y divide-gray-100">
<!-- Data rows will be injected here by JavaScript -->
</tbody>
</table>
</div>
<!-- NEW: Calendar View Container -->
<div id="calendar-view-container" class="hidden">
<div class="flex justify-center items-center mb-4 space-x-4">
<button id="prev-month-btn" class="px-3 py-1 bg-gray-200 rounded-lg hover:bg-gray-300 text-gray-700">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</button>
<h3 id="current-month-year" class="text-xl font-bold text-gray-800"></h3>
<button id="next-month-btn" class="px-3 py-1 bg-gray-200 rounded-lg hover:bg-gray-300 text-gray-700">
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</button>
</div>
<!-- Calendar Grid now uses 7 columns for month view -->
<div id="monthly-events-grid" class="calendar-grid">
<!-- Monthly cards will be injected here -->
</div>
<p id="no-calendar-events" class="text-center py-6 text-gray-500 font-medium hidden">ไม่พบกิจกรรมในเดือนนี้</p>
</div>
<!-- Pagination Controls -->
<div id="pagination-controls" class="flex justify-between items-center mt-6">
<!-- Summary will be here -->
<p id="pagination-summary" class="text-sm text-gray-600"></p>
<!-- Buttons will be here -->
<nav id="pagination-nav" class="flex space-x-1" aria-label="Pagination">
<!-- Pagination buttons will be injected here by JavaScript -->
</nav>
</div>
</div>
<!-- REMOVED: Admin Dashboard HTML section -->
</div>
<!-- Simple Custom Modal for Alerts/Confirms (Since alert() is disallowed) -->
<div id="custom-modal" class="fixed inset-0 bg-gray-900 bg-opacity-75 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
<div class="bg-white rounded-xl shadow-2xl p-6 w-11/12 max-w-sm transform transition-all">
<h3 id="modal-title" class="text-xl font-bold text-gray-800 mb-4"></h3>
<p id="modal-message" class="text-gray-600 mb-6"></p>
<div id="modal-actions" class="flex justify-end space-x-3">
<!-- Buttons will be added here -->
</div>
</div>
</div>
<!-- Firebase SDK Imports (MUST be type="module") -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, addDoc, setDoc, deleteDoc, onSnapshot, collection, query, orderBy, where } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// --- Global State and Constants ---
let db;
let auth;
let userId = null;
let allActivities = [];
let state = {
currentPage: 1,
itemsPerPage: 10,
isAdmin: false, // ALWAYS FALSE IN PUBLIC VIEW
isAuthReady: false,
isEditing: false,
editingId: null,
selectedCategory: 'all',
selectedYear: 'all',
availableYears: [],
viewMode: 'table',
currentCalendarDate: new Date(),
searchTerm: '',
};
// Admin Credential (Not used in public view)
const HARDCODED_ADMIN_PASSWORD = "Aminsatit2568";
// ---------------------------------------------------------------------
// --- FIREBASE HOSTING CONFIGURATION (กรุณาแทนที่ด้วยค่าจริงของคุณ) ---
// ---------------------------------------------------------------------
// 1. **FIREBASE_CONFIG:** นำค่า Config จาก Project Settings ของ Firebase มาใส่
const FIREBASE_CONFIG = {
apiKey: "AIzaSyBslDkcJHe5QIvJ6Z_78hgXodYzoCvlZCI",
authDomain: "academicsystem-e92e5.firebaseapp.com",
projectId: "academicsystem-e92e5",
storageBucket: "academicsystem-e92e5.firebasestorage.app",
messagingSenderId: "1043708760471",
appId: "1:1043708760471:web:8324d0b113e249ba27f4d2" // รหัส App ID
};
// 2. **INITIAL_AUTH_TOKEN:** ตั้งค่าเป็น null เพราะไม่ได้ใช้ Custom Token จาก Canvas
const INITIAL_AUTH_TOKEN = null;
// 3. **COLLECTION_PATH:** ตั้งชื่อ Collection Path ใหม่ (ต้องตรงกับ Firestore Security Rules)
const COLLECTION_PATH = `academic_calendar_data`;
// ---------------------------------------------------------------------
// --- Utility Functions ---
// Converts ISO date (YYYY-MM-DD) to Thai date string (DD Mon. YYYY BE)
function isoToThaiDisplay(isoDateStr) {
if (!isoDateStr) return '';
const [yearG, monthG, dayG] = isoDateStr.split('-').map(Number);
// Month is 0-indexed in JS, so monthG - 1
const date = new Date(yearG, monthG - 1, dayG);
const day = date.getDate();
const yearBE = date.getFullYear() + 543;
const monthNamesThai = [
'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.',
'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'
];
const month = monthNamesThai[date.getMonth()];
return `${day} ${month} ${yearBE}`;
}
// Converts Thai date string (DD Mon. YYYY BE) to ISO date (YYYY-MM-DD)
function formatToIsoDate(thaiDateStr) {
if (!thaiDateStr) return '';
const parts = thaiDateStr.trim().split(/\s+/);
if (parts.length < 3) return '';
const day = parseInt(parts[0], 10);
const monthMap = {
'ม.ค.': 0, 'ก.พ.': 1, 'มี.ค.': 2, 'เม.ย.': 3, 'พ.ค.': 4, 'มิ.ย.': 5,
'ก.ค.': 6, 'ส.ค.': 7, 'ก.ย.': 8, 'ต.ค.': 9, 'พ.ย.': 10, 'ธ.ค.': 11
};
const month = monthMap[parts[1]];
let year = parseInt(parts[2], 10);
// Convert Buddhist year (25XX) to Gregorian year (19XX/20XX)
if (year > 2500) {
year -= 543;
}
if (isNaN(day) || isNaN(month) || isNaN(year)) return '';
// Format to YYYY-MM-DD
const dateObj = new Date(year, month, day);
const isoMonth = (dateObj.getMonth() + 1).toString().padStart(2, '0');
const isoDay = dateObj.getDate().toString().padStart(2, '0');
return `${dateObj.getFullYear()}-${isoMonth}-${isoDay}`;
}
// Function to parse Thai Date string (e.g., "25 ก.พ. 2568") to a sortable Date object
function parseThaiDate(dateStr) {
if (!dateStr) return null;
// Remove spaces and split
const parts = dateStr.trim().split(/\s+/);
if (parts.length < 3) return null;
const day = parseInt(parts[0], 10);
const monthMap = {
'ม.ค.': 0, 'ก.พ.': 1, 'มี.ค.': 2, 'เม.ย.': 3, 'พ.ค.': 4, 'มิ.ย.': 5,
'ก.ค.': 6, 'ส.ค.': 7, 'ก.ย.': 8, 'ต.ค.': 9, 'พ.ย.': 10, 'ธ.ค.': 11
};
const month = monthMap[parts[1]];
let year = parseInt(parts[2], 10);
// Convert Buddhist year (25XX) to Gregorian year (19XX/20XX)
if (year > 2500) {
year -= 543;
}
if (isNaN(day) || isNaN(month) || isNaN(year)) return '';
// Create Date object (Month is 0-indexed in JS)
const date = new Date(year, month, day);
return date.getTime(); // Return timestamp for sorting
}
// NEW: Function to display the date or date range in the table
function formatDisplayDate(startThai, endThai) {
if (!startThai || !endThai) return 'N/A';
if (startThai === endThai) return startThai; // Single day event
// Check if only the day differs (e.g., 15 ม.ค. 2568 - 18 ม.ค. 2568)
const startParts = startThai.trim().split(/\s+/);
const endParts = endThai.trim().split(/\s+/);
if (startParts.length >= 3 && endParts.length >= 3 &&
startParts[1] === endParts[1] && startParts[2] === endParts[2]) {
// Same month and year, show condensed day range
return `${startParts[0]}-${endParts[0]} ${endParts[1]} ${endParts[2]}`;
}
return `${startThai} - ${endThai}`;
}
// Function to format Date to Thai Month (for Calendar View)
function formatThaiMonth(dateObj) {
const months = [
"มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน",
"กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"
];
const monthIndex = dateObj.getMonth();
const yearBE = dateObj.getFullYear() + 543;
return `${months[monthIndex]} ${yearBE}`;
}
/** Shows the custom modal with dynamic content and buttons (Simplified for public view) */
function showModal(title, message, buttons) {
// Only show informational modals in public view, not security/admin related ones
if (title === 'ไม่อนุญาต' || title === 'เข้าสู่ระบบ Admin') return;
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-message').textContent = message;
const actionsDiv = document.getElementById('modal-actions');
actionsDiv.innerHTML = '';
buttons.forEach(btn => {
const buttonEl = document.createElement('button');
buttonEl.className = btn.className;
buttonEl.textContent = btn.text;
buttonEl.onclick = () => {
hideModal();
if (btn.action) btn.action();
};
actionsDiv.appendChild(buttonEl);
});
document.getElementById('custom-modal').classList.remove('hidden');
}
/** Hides the custom modal */
function hideModal() {
document.getElementById('custom-modal').classList.add('hidden');
}
// --- Firebase Initialization and Authentication ---
async function initFirebase() {
if (!FIREBASE_CONFIG || !FIREBASE_CONFIG.apiKey) {
console.error("Firebase config is missing or invalid.");
document.getElementById('error-message').classList.remove('hidden');
return;
}
try {
const app = initializeApp(FIREBASE_CONFIG);
db = getFirestore(app);
auth = getAuth(app);
// In public view, we only need to sign in anonymously for read access (if rules require it)
// We use onAuthStateChanged to ensure the user object (and thus read permission) is established
await signInAnonymously(auth);
onAuthStateChanged(auth, (user) => {
if (user) {
userId = user.uid;
state.isAuthReady = true;
console.log("Firebase initialized. User ID:", userId);
loadActivities();
} else {
console.log("No user signed in.");
// If authentication fails, show error message
document.getElementById('error-message').classList.remove('hidden');
}
});
} catch (error) {
console.error("Error initializing Firebase:", error);
document.getElementById('error-message').classList.remove('hidden');
}
}
// --- Data Fetching and Real-time Listener ---
function loadActivities() {
if (!db || !state.isAuthReady) return;
// Ensure the error message is hidden at the start of loading
document.getElementById('error-message').classList.add('hidden');
const q = query(collection(db, COLLECTION_PATH));
onSnapshot(q, (querySnapshot) => {
allActivities = [];
const years = new Set();
querySnapshot.forEach((doc) => {
const data = {
id: doc.id,
...doc.data(),
// NEW: Add sortable timestamp based on START date string
sortTimestamp: parseThaiDate(doc.data().startDate)
};
allActivities.push(data);
if (data.academicYear) {
years.add(data.academicYear);
}
});
// 1. Sort by Date (ascending: nearest event first)
allActivities.sort((a, b) => {
// Prioritize activities with valid sortTimestamp
if (a.sortTimestamp && b.sortTimestamp) {
return a.sortTimestamp - b.sortTimestamp;
}
// Fallback to primary timestamp if date is invalid (newest first for safety)
return (b.timestamp || 0) - (a.timestamp || 0);
});
// 2. Update available years
state.availableYears = Array.from(years).sort().reverse();
renderYearFilter();
renderView(); // Render the current view mode
}, (error) => {
console.error("Error listening to activities:", error);
// Show error on snapshot failure
document.getElementById('error-message').classList.remove('hidden');
});
}
function renderYearFilter() {
const select = document.getElementById('year-filter');
const currentYear = state.selectedYear;
select.innerHTML = '';
const allOption = document.createElement('option');
allOption.value = 'all';
allOption.textContent = 'ทั้งหมด';
select.appendChild(allOption);
state.availableYears.forEach(year => {
const option = document.createElement('option');
option.value = year;
option.textContent = year;
select.appendChild(option);
});
if (state.availableYears.includes(currentYear)) {
select.value = currentYear;
state.selectedYear = currentYear;
} else {
select.value = 'all';
state.selectedYear = 'all';
}
}
// --- Filtering Logic ---
function getFilteredActivities() {
const { selectedCategory, selectedYear, searchTerm } = state;
return allActivities.filter(activity => {
// Category Filter (ระดับการศึกษา)
const matchesCategory = selectedCategory === 'all' || activity.category === selectedCategory;
// Year Filter
const matchesYear = selectedYear === 'all' || activity.academicYear === selectedYear;
// Search Filter (NEW)
let matchesSearch = true;
if (searchTerm) {
const term = searchTerm.toLowerCase();
// Search against Name, Location, and Responsible Person
matchesSearch = (activity.name && activity.name.toLowerCase().includes(term)) ||
(activity.location && activity.location.toLowerCase().includes(term)) ||
(activity.responsiblePerson && activity.responsiblePerson.toLowerCase().includes(term));
}
return matchesCategory && matchesYear && matchesSearch;
});
}
// --- Rendering Functions ---
// NEW: Main render function to switch between Table and Calendar view
function renderView() {
const tableContainer = document.getElementById('calendar-data-view');
const calendarContainer = document.getElementById('calendar-view-container');
const viewTitle = document.getElementById('view-title');
const paginationControls = document.getElementById('pagination-controls');
const filteredActivities = getFilteredActivities();
const totalItems = filteredActivities.length;
if (state.viewMode === 'table') {
tableContainer.classList.remove('hidden');
calendarContainer.classList.add('hidden');
paginationControls.classList.remove('hidden');
viewTitle.textContent = `รายการกิจกรรมวิชาการทั้งหมด (${totalItems} รายการ) - มุมมองตาราง`;
renderTable(filteredActivities);
} else { // Calendar View
tableContainer.classList.add('hidden');
calendarContainer.classList.remove('hidden');
paginationControls.classList.add('hidden');
viewTitle.textContent = `รายการกิจกรรมวิชาการทั้งหมด (${totalItems} รายการ) - มุมมองปฏิทินรายเดือน`;
renderCalendarView(filteredActivities);
}
}
// Updated renderTable
function renderTable(filteredActivities) {
const tableBody = document.getElementById('activities-table-body');
tableBody.innerHTML = '';
const totalItems = filteredActivities.length;
const totalPages = Math.ceil(totalItems / state.itemsPerPage);
const start = (state.currentPage - 1) * state.itemsPerPage;
let end = start + state.itemsPerPage;
if (state.itemsPerPage === 9999) end = totalItems;
const paginatedActivities = filteredActivities.slice(start, end);
if (paginatedActivities.length === 0) {
let message = 'ไม่พบกิจกรรมในปฏิทิน';
// Note: isAdmin is false, so colspan is 7
tableBody.innerHTML = `<tr><td colspan="7" class="py-4 text-center text-gray-500">${message}</td></tr>`;
}
// 4. Render rows
paginatedActivities.forEach((activity, index) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-indigo-50/50 transition duration-150';
const seq = start + index + 1;
// Display date range
const dateRangeDisplay = formatDisplayDate(activity.startDate, activity.endDate);
// Updated Column Order: ลำดับ, โครงการ/กิจกรรม, สถานที่, วันที่, ระยะเวลา, ปีการศึกษา, ผู้รับผิดชอบโครงการ
tr.innerHTML = `
<td class="px-3 py-3 whitespace-nowrap text-sm text-gray-500 font-mono">${seq}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-800">${activity.name || 'N/A'}</td>
<td class="px-4 py-3 text-sm text-gray-700">${activity.location || 'N/A'}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-blue-600 font-semibold">${dateRangeDisplay || 'N/A'}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">${activity.duration || 'N/A'}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-pink-600 font-bold">${activity.academicYear || '-'}</td>
<td class="px-4 py-3 text-sm text-gray-700">${activity.responsiblePerson || '-'}</td> <!-- NEW Data Cell -->
<!-- REMOVED: Admin actions column -->
`;
tableBody.appendChild(tr);
});
lucide.createIcons();
// Ensure Admin header is hidden (it is removed from HTML now, but keeping this check for safety)
document.getElementById('admin-actions-header').classList.add('hidden');
renderPagination(totalItems, totalPages);
document.getElementById('calendar-data-view').classList.remove('hidden');
}
// Helper function to check if an event spans a specific day
function getEventsForDay(events, checkDate) {
// Ensure checkDate is normalized to start of day for comparison
checkDate.setHours(0, 0, 0, 0);
const checkTimestamp = checkDate.getTime();
return events.filter(event => {
// Event Start Timestamp
const startTimestamp = event.sortTimestamp;
// Event End Timestamp (parsed from Thai format)
const endThaiDate = event.endDate;
const endIsoDate = formatToIsoDate(endThaiDate);
if (!startTimestamp || !endIsoDate) return false;
// Convert end date ISO to timestamp and adjust to end of day for proper range check
const endDateObj = new Date(endIsoDate);
endDateObj.setHours(23, 59, 59, 999);
const adjustedEndTimestamp = endDateObj.getTime();
// Check if the event period includes the checkDate
return checkTimestamp >= startTimestamp && checkTimestamp <= adjustedEndTimestamp;
});
}
// Render Calendar View (Major overhaul for monthly grid)
function renderCalendarView(filteredActivities) {
const grid = document.getElementById('monthly-events-grid');
const title = document.getElementById('current-month-year');
const noEventsMessage = document.getElementById('no-calendar-events');
grid.innerHTML = '';
const currentMonth = state.currentCalendarDate.getMonth();
const currentYear = state.currentCalendarDate.getFullYear();
title.textContent = formatThaiMonth(state.currentCalendarDate);
// 1. Get all events relevant to the current month (events that start or end in this month, or span across it)
const relevantEvents = filteredActivities.filter(activity => {
const startTimestamp = activity.sortTimestamp;
const endTimestamp = parseThaiDate(activity.endDate);
if (!startTimestamp || !endTimestamp) return false;
const startMonth = new Date(startTimestamp).getMonth();
const startYear = new Date(startTimestamp).getFullYear();
const endMonth = new Date(endTimestamp).getMonth();
const endYear = new Date(endTimestamp).getFullYear();
// Check if the event starts, ends, or spans this month
// The event is relevant if its period overlaps with any day in the current month.
const monthStart = new Date(currentYear, currentMonth, 1).getTime();
const monthEnd = new Date(currentYear, currentMonth + 1, 0).getTime();
// Adjusted End Timestamp for comparison to cover the whole end day
const adjustedEndTimestamp = new Date(endTimestamp);
adjustedEndTimestamp.setHours(23, 59, 59, 999);
return startTimestamp <= monthEnd && adjustedEndTimestamp.getTime() >= monthStart;
});
// 2. Setup Calendar Grid Structure (Days of the week header)
const dayNames = ['อาทิตย์', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', 'เสาร์'];
const headerHtml = dayNames.map(day =>
`<div class="text-center font-bold text-sm text-gray-600 py-2 border-b-2 border-gray-300">${day}</div>`
).join('');
grid.innerHTML = headerHtml;
// 3. Calculate Days (Calendar generation logic)
const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0);
const lastDayPrevMonth = new Date(currentYear, currentMonth, 0);
let startDayIndex = firstDayOfMonth.getDay(); // 0 for Sunday, 1 for Monday...
const daysInMonth = lastDayOfMonth.getDate();
const daysInPrevMonth = lastDayPrevMonth.getDate();
// Calculate total cells needed to display full weeks
const totalCells = startDayIndex + daysInMonth + (7 - (startDayIndex + daysInMonth) % 7) % 7;
// 4. Populate Cells
for (let i = 0; i < totalCells; i++) {
const cell = document.createElement('div');
cell.className = 'calendar-card flex flex-col space-y-1 overflow-hidden';
let date;
let dayNumber;
let isCurrentMonth = false;
if (i < startDayIndex) {
// Padding days from previous month
dayNumber = daysInPrevMonth - startDayIndex + i + 1;
date = new Date(currentYear, currentMonth - 1, dayNumber);
cell.classList.add('bg-gray-50', 'text-gray-400');
} else if (i >= startDayIndex && i < startDayIndex + daysInMonth) {
// Days in current month
dayNumber = i - startDayIndex + 1;
date = new Date(currentYear, currentMonth, dayNumber);
cell.classList.add('bg-white', 'text-gray-800');
isCurrentMonth = true;
} else {
// Padding days from next month
dayNumber = i - (startDayIndex + daysInMonth) + 1;
date = new Date(currentYear, currentMonth + 1, dayNumber);
cell.classList.add('bg-gray-50', 'text-gray-400');
}
// Day number display
cell.innerHTML += `<h5 class="text-sm font-extrabold mb-1 ${isCurrentMonth ? 'text-blue-700' : 'text-gray-400'}">${dayNumber}</h5>`;
if (isCurrentMonth) {
const dayEvents = getEventsForDay(relevantEvents, date);
// Sort events that start on this day first, then by duration
dayEvents.sort((a, b) => {
const aStartsToday = new Date(a.sortTimestamp).getDate() === dayNumber;
const bStartsToday = new Date(b.sortTimestamp).getDate() === dayNumber;
if (aStartsToday && !bStartsToday) return -1;
if (!aStartsToday && bStartsToday) return 1;
return 0;
});
const eventHtml = dayEvents.map(event => {
// Display Duration and Date Range
const dateRangeDisplay = formatDisplayDate(event.startDate, event.endDate);
return `
<div class="border-l-4 p-1 rounded-r-md text-xs"
style="border-color: ${getCategoryColor(event.category)}; background-color: ${getCategoryColor(event.category, true)}">
<span class="font-bold block truncate" title="${event.name}">${event.name}</span>
<span class="text-xs text-gray-600 block truncate" title="ระยะเวลา">${event.duration || ''}</span>
<span class="text-xs text-blue-800 block truncate" title="วันที่จัดกิจกรรม">
<i data-lucide="calendar-check" class="w-3 h-3 inline-block mr-0.5"></i> ${dateRangeDisplay}
</span>
</div>
`;
}).join('');
cell.innerHTML += eventHtml;
}
grid.appendChild(cell);
}
if (relevantEvents.length === 0 && (new Date().getMonth() === currentMonth && new Date().getFullYear() === currentYear)) {
// Only show 'no events' if the current month has no events
// If we are showing future months, it's better to show the calendar grid empty.
} else if (relevantEvents.length === 0) {
// Do nothing, show empty calendar grid.
}
noEventsMessage.classList.add('hidden'); // Always hide the message since we are showing the grid.
lucide.createIcons();
}
// Helper function for calendar view
function getCategoryColor(category, light = false) {
const colors = {
'ปฐมวัย': light ? '#ffe4e6' : '#f43f5e', // Pink
'ประถมศึกษา': light ? '#ecfdf5' : '#10b981', // Emerald
'มัธยมศึกษา': light ? '#eff6ff' : '#3b82f6', // Blue
'ทั่วไป': light ? '#fefce8' : '#f59e0b', // Amber
};
return colors[category] || (light ? '#f3f4f6' : '#6b7280'); // Default Gray
}
function renderPagination(totalItems, totalPages) {
const paginationNav = document.getElementById('pagination-nav');
const summary = document.getElementById('pagination-summary');
paginationNav.innerHTML = '';
const startItem = totalItems > 0 ? (state.currentPage - 1) * state.itemsPerPage + 1 : 0;
let endItem = Math.min(startItem + state.itemsPerPage - 1, totalItems);
if (state.itemsPerPage === 9999) endItem = totalItems;
summary.textContent = `แสดงรายการที่ ${startItem} ถึง ${endItem} จากทั้งหมด ${totalItems} รายการ`;
if (state.itemsPerPage === 9999 || totalPages <= 1) return;
// ... (Pagination button logic remains the same)
const prevButton = document.createElement('button');
prevButton.textContent = 'ก่อนหน้า';
prevButton.className = `px-3 py-2 text-sm font-medium rounded-lg transition ${state.currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 bg-gray-50 hover:bg-gray-100'}`;
prevButton.disabled = state.currentPage === 1;
prevButton.onclick = () => {
if (state.currentPage > 1) {
state.currentPage--;
renderView();
}
};
paginationNav.appendChild(prevButton);
let startPage = Math.max(1, state.currentPage - 1);
let endPage = Math.min(totalPages, state.currentPage + 1);
if (state.currentPage === 1) endPage = Math.min(totalPages, 3);
if (state.currentPage === totalPages) startPage = Math.max(1, totalPages - 2);
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
pageButton.className = `px-3 py-2 text-sm font-medium rounded-lg transition ${i === state.currentPage ? 'text-white bg-blue-600 shadow-md' : 'text-gray-700 hover:bg-gray-100'}`;
pageButton.onclick = () => {
state.currentPage = i;
renderView();
};
paginationNav.appendChild(pageButton);
}
const nextButton = document.createElement('button');
nextButton.textContent = 'ถัดไป';
nextButton.className = `px-3 py-2 text-sm font-medium rounded-lg transition ${state.currentPage === totalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 bg-gray-50 hover:bg-gray-100'}`;
nextButton.disabled = state.currentPage === totalPages;
nextButton.onclick = () => {
if (state.currentPage < totalPages) {
state.currentPage++;
renderView();
}
};
paginationNav.appendChild(nextButton);
}
// --- Event Handlers for Admin/CRUD Operations ---
// NO ADMIN LOGIC IN PUBLIC VIEW.
// --- Event Listeners Setup ---
function setViewMode(mode) {
state.viewMode = mode;
const tableBtn = document.getElementById('view-table-btn');
const calendarBtn = document.getElementById('view-calendar-btn');
if (mode === 'table') {
tableBtn.classList.add('bg-blue-600', 'text-white', 'shadow-md', 'font-semibold');
tableBtn.classList.remove('text-gray-700', 'hover:bg-gray-200', 'font-medium');
calendarBtn.classList.remove('bg-blue-600', 'text-white', 'shadow-md', 'font-semibold');
calendarBtn.classList.add('text-gray-700', 'hover:bg-gray-200', 'font-medium');
state.currentPage = 1; // Reset page when switching back to table
} else {
calendarBtn.classList.add('bg-blue-600', 'text-white', 'shadow-md', 'font-semibold');
calendarBtn.classList.remove('text-gray-700', 'hover:bg-gray-200', 'font-medium');
tableBtn.classList.remove('bg-blue-600', 'text-white', 'shadow-md', 'font-semibold');
tableBtn.classList.add('text-gray-700', 'hover:bg-gray-200', 'font-medium');
}
renderView();
}
window.onload = () => {
// Initialize Firebase
initFirebase();
// REMOVED: Admin Toggle Button Listener
// Items Per Page Select
document.getElementById('items-per-page').addEventListener('change', (e) => {
state.itemsPerPage = parseInt(e.target.value);
state.currentPage = 1;
renderView();
});
// REMOVED: Activity Form Submission (Add/Edit)
// REMOVED: Cancel Edit Button
// REMOVED: Edit/Delete Delegation for Table Body
// Category Filter Dropdown Listener
document.getElementById('category-filter').addEventListener('change', (e) => {
state.selectedCategory = e.target.value;
state.currentPage = 1;
renderView();
});
// Academic Year Filter Listener
document.getElementById('year-filter').addEventListener('change', (e) => {
state.selectedYear = e.target.value;
state.currentPage = 1;
renderView();
});
// Search Input Listener
document.getElementById('search-input').addEventListener('input', (e) => {
state.searchTerm = e.target.value;
state.currentPage = 1;
renderView();
});
// View Toggle Listeners
document.getElementById('view-table-btn').addEventListener('click', () => setViewMode('table'));
document.getElementById('view-calendar-btn').addEventListener('click', () => setViewMode('calendar'));
// Calendar Navigation Buttons
document.getElementById('prev-month-btn').addEventListener('click', () => {
state.currentCalendarDate.setMonth(state.currentCalendarDate.getMonth() - 1);
renderView();
});
document.getElementById('next-month-btn').addEventListener('click', () => {
state.currentCalendarDate.setMonth(state.currentCalendarDate.getMonth() + 1);
renderView();
});
// Set initial view mode
setViewMode(state.viewMode);
// Initialize Icons
lucide.createIcons();
};
</script>
</body>
</html>