Lịch phòng Lab

0
7


 

 

:root {
--primary: #4f46e5;
--primary-hover: #4338ca;
--bg-body: #f8fafc;
--surface: #ffffff;
--text-main: #0f172a;
--text-sub: #64748b;
--border: #e2e8f0;

--fc-border-color: #e2e8f0;
--fc-today-bg-color: #f1f5f9;
--fc-button-bg-color: #4f46e5;
--fc-button-border-color: #4f46e5;
--fc-button-hover-bg-color: #4338ca;
--fc-button-active-bg-color: #3730a3;
}

* { box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-body);
color: var(--text-main);
margin: 0;
padding: 12px;
-webkit-tap-highlight-color: transparent;
}

.container { max-width: 1400px; margin: 0 auto; }

/* Notification Banner */
.notification-banner {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #fff;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 15px rgba(15, 23, 42, 0.15);
flex-wrap: wrap;
gap: 10px;
}
.notice-left { display: flex; align-items: center; gap: 10px; }
.notice-pulse {
width: 10px; height: 10px; border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
animation: pulse 1.8s infinite;
flex-shrink: 0;
}
@keyframes pulse {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
.notice-text { font-size: 13px; line-height: 1.4; }
.notice-text b { color: #fde047; }
.btn-notice-jump {
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
width: 100%;
text-align: center;
}
@media (min-width: 640px) {
.btn-notice-jump { width: auto; }
}

/* Header Card */
.header-card {
background: var(--surface);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
border: 1px solid var(--border);
margin-bottom: 15px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-direction: column;
gap: 14px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 14px;
}
@media (min-width: 768px) {
.header-top { flex-direction: row; align-items: center; }
body { padding: 20px; }
.header-card { padding: 20px 24px; }
}
.title-group h1 {
font-size: 18px;
font-weight: 700;
margin: 0;
color: #1e1b4b;
}
.title-group p { margin: 4px 0 0 0; font-size: 12.5px; color: var(--text-sub); }

.btn-group-header {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
width: 100%;
}
@media (min-width: 768px) {
.btn-group-header { width: auto; }
}

.btn {
padding: 8px 12px;
border-radius: 6px;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 38px;
touch-action: manipulation;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-outline { background: #fff; border-color: var(--border); color: var(--text-main); }
.btn-outline:active { background: #f1f5f9; }

.badge-admin {
padding: 4px 8px;
border-radius: 9999px;
font-size: 11px;
font-weight: 600;
background: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
display: none;
}

/* Metric Grid */
.metric-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (min-width: 768px) {
.metric-grid { grid-template-columns: repeat(4, 1fr); gap: 15px; }
}
.metric-box {
background: #f8fafc;
border: 1px solid var(--border);
padding: 10px 12px;
border-radius: 8px;
}
.metric-label { font-size: 11px; color: var(--text-sub); font-weight: 600; text-transform: uppercase; }
.metric-value { font-size: 18px; font-weight: 700; color: #0f172a; margin-top: 4px; }

/* Control Bar */
.control-bar {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--surface);
padding: 12px 14px;
border-radius: 10px;
border: 1px solid var(--border);
margin-bottom: 15px;
}
@media (min-width: 640px) {
.control-bar { flex-direction: row; justify-content: space-between; align-items: center; }
}
.filter-group { display: flex; align-items: center; gap: 8px; width: 100%; }
.select-input {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
background: #fff;
color: var(--text-main);
outline: none;
min-height: 40px;
}

/* Calendar Card */
.calendar-card {
background: var(--surface);
padding: 10px;
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
}
@media (min-width: 768px) {
.calendar-card { padding: 20px; }
}

/* FullCalendar Mobile Adjustments */
.fc .fc-toolbar {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
@media (min-width: 768px) {
.fc .fc-toolbar { flex-direction: row; justify-content: space-between; align-items: center; }
}
.fc .fc-toolbar-title { font-size: 16px !important; text-align: center; }
.fc .fc-button {
padding: 6px 10px !important;
font-size: 12px !important;
min-height: 36px;
}

.event-badge-month {
width: 100%;
padding: 4px 5px;
font-size: 11px;
border-radius: 3px;
display: flex;
align-items: center;
gap: 4px;
color: #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 22px;
}
.event-card-time {
padding: 4px 6px;
height: 100%;
display: flex;
flex-direction: column;
color: #fff;
font-size: 11px;
line-height: 1.3;
}

/* SweetAlert Form Responsive */
.swal-form-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
text-align: left;
margin-top: 6px;
}
@media (min-width: 550px) {
.swal-form-grid { grid-template-columns: 1fr 1fr; gap: 12px; }
.swal-form-full { grid-column: span 2; }
}
.swal-label { font-size: 12px; font-weight: 600; color: #334155; margin-bottom: 4px; display: block; }
.swal-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 14px;
min-height: 42px;
}

.fc-list-event td { padding: 10px !important; }
.event-list-row { font-size: 13px; }

/* 1. Đổi màu tiêu đề các thứ (CN, Th 2, Th 3...) -> Xanh đen đậm */
.fc .fc-col-header-cell-cushion {
color: #0f172a !important; /* Đổi từ màu vàng sang màu xanh đen sắc nét */
font-weight: 700 !important;
font-size: 14px !important;
text-decoration: none !important;
}

/* 2. Đổi màu số ngày (1, 2, 3...) -> Đậm rõ nét */
.fc .fc-daygrid-day-number {
color: #1e293b !important; /* Màu số ngày rõ nét, tương phản cao */
font-weight: 600 !important;
font-size: 13.5px !important;
text-decoration: none !important;
}

/* 3. (Tùy chọn) Làm mờ số ngày của tháng trước/tháng sau (30, 31) để đỡ rối mắt */
.fc .fc-day-other .fc-daygrid-day-number {
color: #94a3b8 !important; /* Màu xám nhạt */
opacity: 0.7;
}

Đang tải trạng thái phòng máy...

🖥️ Lịch Phòng Máy

Chạm vào ô ngày để đặt lịch

🛡️ Admin
Lượt xem này
0
Tổng thời lượng
0 giờ
Tổng sinh viên
0 SV
Phòng đang xem
Phòng GIS 1


Phòng GIS 1
Phòng GIS 2

-- Tất cả đơn vị --
Khoa TNĐ
Khoa khác 1
Khoa khác 2
Khoa khác 3
Đơn vị khác

const ADMIN_PIN_DEFAULT = "1234";
const STORAGE_KEY = "sys_room_bookings";

let isAdmin = false;
let calendar;
let currentRoomId = "LAB_01";
let currentDeptFilter = "ALL";
let nextUpcomingEvent = null;

const DEPT_COLORS = {
'Khoa TNĐ': '#2563eb',
'Khoa khác 1': '#0891b2',
'Khoa khác 2': '#059669',
'Khoa khác 3': '#d97706',
'Khác': '#7c3aed',
'MAINTENANCE': '#475569'
};

const ROOM_MAP = {
'LAB_01': 'Phòng GIS 1',
'LAB_02': 'Phòng GIS 2'
};

const isMobile = window.innerWidth < 768;

function escapeHTML(str) {
if (str === null || str === undefined) return '';
return String(str).replace(/[&'"]/g, tag => ({
'&': '&', '': '>', "'": ''', '"': '"'
}[tag] || tag));
}

function getStoredBookings() {
try {
const data = JSON.parse(localStorage.getItem(STORAGE_KEY));
return Array.isArray(data) ? data : [];
} catch (err) {
console.error("Không đọc được dữ liệu:", err);
return [];
}
}

function saveBookings(data) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
updateUpcomingNotice();
updateStatistics();
}

function generateId(prefix = 'EV') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

function formatDateTime(value) {
return new Date(value).toLocaleString('vi-VN', {
weekday: 'short', day: '2-digit', month: '2-digit',
year: 'numeric', hour: '2-digit', minute: '2-digit'
});
}

function formatDateOnly(value) {
return new Date(value).toLocaleDateString('vi-VN', {
weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric'
});
}

function formatTime(value) {
return new Date(value).toLocaleTimeString('vi-VN', {
hour: '2-digit', minute: '2-digit'
});
}

function getEventGroupId(eventObj) {
if (!eventObj) return null;
return eventObj.groupId || eventObj.extendedProps?.groupId || null;
}

function getSeriesBookings(groupId) {
if (!groupId) return [];
return getStoredBookings()
.filter(e => e.groupId === groupId)
.sort((a, b) => new Date(a.start) - new Date(b.start));
}

function getSeriesSummary(eventObj) {
const groupId = getEventGroupId(eventObj);
if (!groupId) return '';
const series = getSeriesBookings(groupId);
if (!series.length) return '';
return `🔁 ${series.length} buổi | ${formatDateOnly(series[0].start)} → ${formatDateOnly(series[series.length - 1].start)}`;
}

document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar');

calendar = new FullCalendar.Calendar(calendarEl, {
initialView: isMobile ? 'timeGridDay' : 'dayGridMonth',
locale: 'vi',
slotMinTime: '07:00:00',
slotMaxTime: '21:30:00',
slotDuration: '00:30:00',
allDaySlot: false,
selectable: true,
selectMirror: true,
selectOverlap: false,
height: 'auto',
dayMaxEvents: 2,
nowIndicator: true,

// Cấu hình phản hồi cảm ứng nhạy trên điện thoại
selectLongPressDelay: 200,
eventLongPressDelay: 0,

headerToolbar: {
left: 'prev,next today',
center: 'title',
right: isMobile
? 'dayGridMonth,timeGridDay,listMonth'
: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth,listYear'
},
buttonText: {
today: 'Nay',
month: 'Tháng',
week: 'Tuần',
day: 'Ngày',
listMonth: 'DS',
listYear: 'Năm'
},

events: function(fetchInfo, successCallback) {
const allEvents = getStoredBookings();
const filtered = allEvents.filter(e => {
const matchRoom = e.roomId === currentRoomId;
const matchDept = currentDeptFilter === 'ALL' || e.department === currentDeptFilter;
return matchRoom && matchDept;
});
successCallback(filtered);
},

eventContent: function(arg) {
const p = arg.event.extendedProps || {};
const groupId = getEventGroupId(arg.event);
const viewType = arg.view.type;
const container = document.createElement('div');

if (viewType === 'dayGridMonth') {
container.className = 'event-badge-month' + (p.isMaintenance ? ' maintenance-bg' : '');
const startTime = arg.event.start
? arg.event.start.toLocaleTimeString('vi-VN', {hour:'2-digit', minute:'2-digit'})
: '';
container.innerHTML = `
${escapeHTML(startTime)} ${groupId ? '🔁' : ''} ${escapeHTML(p.bookerName)}`;
} else if (viewType.startsWith('timeGrid')) {
container.className = 'event-card-time' + (p.isMaintenance ? ' maintenance-bg' : '');
container.innerHTML = `

${groupId ? '🔁 ' : ''}👤 ${escapeHTML(p.bookerName)}
📝 ${escapeHTML(p.purpose)}

`;
} else {
container.innerHTML = `

${groupId ? '🔁' : ''}
${escapeHTML(p.bookerName)}: ${escapeHTML(p.purpose)}
`;
}
return { domNodes: [container] };
},

// Xử lý chạm trên điện thoại: Dùng dateClick để tap là mở ngay
dateClick: function(info) {
const start = new Date(info.date);
let end = new Date(start);

// Nếu ở view tháng (không có giờ cụ thể), mặc định từ 8h đến 11h30
if (info.allDay) {
start.setHours(8, 0, 0, 0);
end.setHours(11, 30, 0, 0);
} else {
end.setTime(start.getTime() + 90 * 60 * 1000); // 1.5 tiếng
}

openBookingModal({
start: start,
end: end,
isMonthView: info.allDay
});
},

select: function(info) {
// Trên Desktop, khi quét chuột chọn khung giờ
if (!isMobile) {
openBookingModal({
start: info.start,
end: info.end,
isMonthView: info.view.type === 'dayGridMonth'
});
}
},

eventClick: handleEventClick,
datesSet: function() {
updateStatistics();
updateManagementButton();
}
});

calendar.render();
updateUpcomingNotice();
updateManagementButton();
setInterval(updateUpcomingNotice, 60000);
});

// ============================================================
// THÔNG BÁO LỊCH GẦN NHẤT
// ============================================================
function updateUpcomingNotice() {
const bookings = getStoredBookings();
const now = new Date();

const upcoming = bookings
.filter(e => new Date(e.end) > now)
.sort((a, b) => new Date(a.start) - new Date(b.start));

const bannerText = document.getElementById('upcomingText');
const jumpBtn = document.getElementById('btnNoticeJump');

if (!upcoming.length) {
bannerText.innerHTML = 'Hiện tại không có lịch phòng máy nào sắp tới.';
jumpBtn.style.display = 'none';
nextUpcomingEvent = null;
return;
}

nextUpcomingEvent = upcoming[0];
const evStart = new Date(nextUpcomingEvent.start);
const diffMinutes = Math.round((evStart - now) / 60000);

let timeMsg = diffMinutes <= 0
? '[Đang diễn ra]'
: `[Sau ${Math.floor(diffMinutes / 60)}h]`;

const roomLabel = ROOM_MAP[nextUpcomingEvent.roomId] || nextUpcomingEvent.roomId;
bannerText.innerHTML = `🔔 ${escapeHTML(roomLabel)}: ${formatTime(nextUpcomingEvent.start)} - ${escapeHTML(nextUpcomingEvent.bookerName)} ${timeMsg}`;
jumpBtn.style.display = 'block';
}

function jumpToUpcomingEvent() {
if (!nextUpcomingEvent) return;

if (currentRoomId !== nextUpcomingEvent.roomId) {
currentRoomId = nextUpcomingEvent.roomId;
document.getElementById('roomFilter').value = currentRoomId;
document.getElementById('statCurrentRoom').innerText =
document.getElementById('roomFilter').selectedOptions[0].text;
calendar.refetchEvents();
}

calendar.changeView('timeGridDay', nextUpcomingEvent.start);
}

// ============================================================
// ĐẶT LỊCH (MỞ DIALOG TỐI ƯU CẢ MOBILE & DESKTOP)
// ============================================================
async function openBookingModal(timeData) {
if (timeData.start < new Date()) {
Swal.fire({
icon: 'warning',
title: 'Thời gian đã qua',
text: 'Không thể đặt phòng vào thời điểm trong quá khứ!'
});
calendar.unselect();
return;
}

const roomName = document.getElementById('roomFilter').selectedOptions[0].text;
const defaultStartStr = String(timeData.start.getHours()).padStart(2, '0') + ':' + String(timeData.start.getMinutes()).padStart(2, '0');
const defaultEndStr = String(timeData.end.getHours()).padStart(2, '0') + ':' + String(timeData.end.getMinutes()).padStart(2, '0');

const formHtml = `

Phòng: ${escapeHTML(roomName)} | Ngày: ${formatDateOnly(timeData.start)}

Khoa TNĐ
Khoa khác 1
Khoa khác 2
Khoa khác 3
Đơn vị khác

Lặp lại hàng tuần (Tạo chuỗi)

${isAdmin ? `

[Admin] Khóa phòng để Bảo trì

` : ''}

`;

const { value: formValues } = await Swal.fire({
title: 'Đăng Ký Phòng Máy',
html: formHtml,
width: window.innerWidth {
const recurCheck = document.getElementById('swalRecurring');
const recurBox = document.getElementById('recurOptions');
recurCheck.addEventListener('change', () => {
recurBox.style.display = recurCheck.checked ? 'block' : 'none';
});
},
preConfirm: () => {
const isMaint = document.getElementById('swalIsMaintenance')?.checked || false;
const name = document.getElementById('swalName').value.trim();
const phone = document.getElementById('swalPhone').value.trim();
const purpose = document.getElementById('swalPurpose').value.trim();
const weeks = parseInt(document.getElementById('swalWeeks')?.value || '1', 10);
const sTime = document.getElementById('swalStartTime').value;
const eTime = document.getElementById('swalEndTime').value;

if (!sTime || !eTime) {
Swal.showValidationMessage('Vui lòng chọn khung giờ hợp lệ!');
return false;
}
if (!isMaint && (!name || !phone || !purpose)) {
Swal.showValidationMessage('Vui lòng điền đủ Tên, SĐT và Mục đích!');
return false;
}

return {
name: isMaint ? 'BẢO TRÌ PHÒNG' : name,
department: isMaint ? 'Admin' : document.getElementById('swalDept').value,
phone,
students: document.getElementById('swalStudents').value || '0',
purpose: purpose || 'Bảo dưỡng kỹ thuật',
isRecurring: document.getElementById('swalRecurring').checked,
weeks,
isMaintenance: isMaint,
sTime,
eTime
};
}
});

if (formValues) {
const [sH, sM] = formValues.sTime.split(':');
const [eH, eM] = formValues.eTime.split(':');

const actualStart = new Date(timeData.start);
const actualEnd = new Date(timeData.start);
actualStart.setHours(parseInt(sH, 10), parseInt(sM, 10), 0, 0);
actualEnd.setHours(parseInt(eH, 10), parseInt(eM, 10), 0, 0);

if (actualEnd <= actualStart) {
Swal.fire({
icon: 'warning',
title: 'Giờ chưa đúng',
text: 'Giờ kết thúc phải sau giờ bắt đầu trong cùng một ngày.'
});
calendar.unselect();
return;
}

saveNewEvents({ start: actualStart, end: actualEnd }, formValues);
}
calendar.unselect();
}

function saveNewEvents(timeInfo, values) {
let bookings = getStoredBookings();
const batchGroupId = values.isRecurring ? generateId('GRP') : null;
const loopCount = values.isRecurring ? values.weeks : 1;
let createdCount = 0;
let skippedCount = 0;

for (let i = 0; i {
if (e.roomId !== currentRoomId) return false;
const eStart = new Date(e.start);
const eEnd = new Date(e.end);
return startInstance eStart;
});

if (isOverlap) {
skippedCount++;
continue;
}

const newEv = {
id: generateId('EV'),
groupId: batchGroupId,
seriesType: values.isRecurring ? 'weekly' : null,
roomId: currentRoomId,
title: values.name + ' - ' + values.purpose,
start: startInstance.toISOString(),
end: endInstance.toISOString(),
backgroundColor: values.isMaintenance
? DEPT_COLORS.MAINTENANCE
: (DEPT_COLORS[values.department] || '#4f46e5'),
bookerName: values.name,
department: values.department,
phone: values.phone,
studentCount: values.students,
purpose: values.purpose,
isMaintenance: values.isMaintenance,
createdAt: new Date().toISOString(),
extendedProps: {
groupId: batchGroupId
}
};

bookings.push(newEv);
createdCount++;
}

if (!createdCount) {
Swal.fire({
icon: 'warning',
title: 'Bị trùng lịch',
text: 'Khung giờ này phòng máy đã có người đặt!'
});
return;
}

saveBookings(bookings);
calendar.refetchEvents();

Swal.fire({
icon: 'success',
title: 'Đặt thành công!',
text: `Đã tạo ${createdCount} buổi thành công.` + (skippedCount ? ` (Bỏ qua ${skippedCount} buổi trùng).` : ''),
timer: 2000,
showConfirmButton: false
});
}

// ============================================================
// CHI TIẾT & HỦY BUỔI / HỦY CHUỖI
// ============================================================
async function handleEventClick(info) {
const ev = info.event;
const p = ev.extendedProps || {};
const groupId = getEventGroupId(ev);
const isSeries = groupId && getSeriesBookings(groupId).length > 1;

const detailHtml = `

⏰ Thời gian: ${formatDateTime(ev.start)} – ${formatTime(ev.end)}

🖥️ Phòng: ${escapeHTML(ROOM_MAP[p.roomId] || p.roomId)}

👤 Người đăng ký: ${escapeHTML(p.bookerName)}

🏢 Đơn vị: ${escapeHTML(p.department)}

📝 Mục đích: ${escapeHTML(p.purpose)}

${isSeries ? `

🔁 Thuộc chuỗi lịch tuần:
${escapeHTML(getSeriesSummary(ev))}

` : ''}

`;

const result = await Swal.fire({
title: p.isMaintenance ? 'Thông Tin Bảo Trì' : 'Chi Tiết Đặt Phòng',
html: detailHtml,
width: window.innerWidth 1;

const formHtml = `

${isSeries ? `

🔁 Chuỗi này có ${series.length} buổi. Chọn phạm vi hủy:

 

Chỉ hủy 1 buổi này (${formatDateOnly(eventObj.start)})

Hủy TOÀN BỘ chuỗi (${series.length} buổi)

` : `

Bạn muốn hủy lịch vào ${formatDateTime(eventObj.start)}?

`}
${!isAdmin ? `

` : `🛡️ Chế độ Admin: Hủy trực tiếp`}

`;

const { value: confirmedValues } = await Swal.fire({
title: isSeries ? 'Hủy Lịch Theo Chuỗi' : 'Hủy Buổi Đặt',
html: formHtml,
width: window.innerWidth {
const scope = isSeries
? (document.querySelector('input[name="cancelScope"]:checked')?.value || 'one')
: 'one';
const inputPhone = document.getElementById('cancelPhoneInput')?.value.trim();

if (!isAdmin && !inputPhone) {
Swal.showValidationMessage('Vui lòng nhập SĐT người đăng ký!');
return false;
}
if (!isAdmin && inputPhone !== p.phone) {
Swal.showValidationMessage('Số điện thoại không khớp!');
return false;
}

return { scope };
}
});

if (!confirmedValues) return;

if (confirmedValues.scope === 'all') {
const count = series.length;
saveBookings(getStoredBookings().filter(e => e.groupId !== groupId));
calendar.refetchEvents();
Swal.fire({ icon: 'success', title: `Đã hủy toàn bộ ${count} buổi!`, timer: 1400, showConfirmButton: false });
} else {
saveBookings(getStoredBookings().filter(e => e.id !== eventObj.id));
calendar.refetchEvents();
Swal.fire({ icon: 'success', title: 'Đã hủy buổi!', timer: 1200, showConfirmButton: false });
}
}

// ============================================================
// TRA CỨU & HỦY TOÀN BỘ CHUỖI NHANH THEO SĐT
// ============================================================
async function openUserLookupModal() {
const { value: phone } = await Swal.fire({
title: 'Tra Cứu / Hủy Lịch',
text: 'Nhập số điện thoại của bạn:',
input: 'tel',
inputPlaceholder: '09xxxxxxxx',
showCancelButton: true,
confirmButtonText: 'Xem lịch',
cancelButtonText: 'Đóng',
confirmButtonColor: '#4f46e5'
});

if (!phone) return;

const all = getStoredBookings();
const userBookings = all.filter(e => e.phone === phone.trim());

if (!userBookings.length) {
Swal.fire({ icon: 'info', title: 'Không tìm thấy', text: `Không có lịch gắn với số ${phone}` });
return;
}

const groupMap = new Map();
const singles = [];

userBookings.forEach(e => {
if (e.groupId) {
if (!groupMap.has(e.groupId)) groupMap.set(e.groupId, []);
groupMap.get(e.groupId).push(e);
} else {
singles.push(e);
}
});

let contentHtml = `

`;

if (groupMap.size > 0) {
contentHtml += `🔁 Chuỗi lặp (${groupMap.size}):
`;
groupMap.forEach((events, gId) => {
events.sort((a, b) => new Date(a.start) - new Date(b.start));
contentHtml += `

${escapeHTML(events[0].purpose)}
${events.length} buổi (${formatDateOnly(events[0].start)})

`;
});
}

if (singles.length > 0) {
contentHtml += `📅 Buổi đơn lẻ (${singles.length}):
`;
singles.forEach(ev => {
contentHtml += `

${escapeHTML(ev.purpose)}
${formatDateTime(ev.start)}

`;
});
}

contentHtml += `

`;

Swal.fire({
title: `Lịch của SĐT ${escapeHTML(phone)}`,
html: contentHtml,
width: window.innerWidth e.groupId === groupId && e.phone === phone).length;
if (!count) return;

const confirm = await Swal.fire({
title: 'Hủy cả chuỗi?',
text: `Xóa toàn bộ ${count} buổi thuộc chuỗi này?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Hủy hết',
cancelButtonText: 'Không',
confirmButtonColor: '#ef4444'
});

if (!confirm.isConfirmed) return;

saveBookings(bookings.filter(e => e.groupId !== groupId));
calendar.refetchEvents();
Swal.fire({ icon: 'success', title: `Đã xóa ${count} buổi!`, timer: 1400, showConfirmButton: false });
}

async function userCancelSingle(eventId) {
saveBookings(getStoredBookings().filter(e => e.id !== eventId));
calendar.refetchEvents();
Swal.fire({ icon: 'success', title: 'Đã hủy buổi!', timer: 1200, showConfirmButton: false });
}

// ============================================================
// QUẢN TRỊ ADMIN
// ============================================================
function updateManagementButton() {
const btn1 = document.getElementById('btnManageSeries');
const btn2 = document.getElementById('btnClearOld');
if (btn1) btn1.style.display = isAdmin ? 'inline-flex' : 'none';
if (btn2) btn2.style.display = isAdmin ? 'inline-flex' : 'none';
}

async function toggleAdminMode() {
if (isAdmin) {
isAdmin = false;
document.getElementById('adminBadge').style.display = 'none';
document.getElementById('btnAdminToggle').innerText = '🔐 Admin';
updateManagementButton();
Swal.fire({icon:'info',title:'Đã thoát Admin',timer:1000,showConfirmButton:false});
return;
}

const { value: pin } = await Swal.fire({
title: 'Đăng nhập Admin',
input: 'password',
inputPlaceholder: 'Mã PIN...',
showCancelButton: true,
confirmButtonText: 'Xác thực',
cancelButtonText: 'Đóng'
});

if (pin === ADMIN_PIN_DEFAULT) {
isAdmin = true;
document.getElementById('adminBadge').style.display = 'inline-block';
document.getElementById('btnAdminToggle').innerText = '🔓 Thoát';
updateManagementButton();
Swal.fire({icon:'success',title:'Đã vào Admin',timer:1200,showConfirmButton:false});
} else if (pin) {
Swal.fire({icon:'error',title:'Sai mã PIN!'});
}
}

async function manageRecurringSeries() {
if (!isAdmin) return;
const bookings = getStoredBookings();
const map = new Map();

bookings.forEach(e => {
if (!e.groupId) return;
if (!map.has(e.groupId)) map.set(e.groupId, []);
map.get(e.groupId).push(e);
});

if (!map.size) {
Swal.fire({ icon: 'info', title: 'Chưa có chuỗi nào' });
return;
}

let html = `

`;
map.forEach((events, gId) => {
const first = events[0];
html += `

${escapeHTML(first.bookerName)}: ${escapeHTML(first.purpose)}
${events.length} buổi (${escapeHTML(ROOM_MAP[first.roomId] || first.roomId)})

`;
});
html += `

`;

Swal.fire({
title: 'Quản Lý Chuỗi Lịch',
html: html,
width: window.innerWidth e.groupId === groupId).length;
saveBookings(bookings.filter(e => e.groupId !== groupId));
calendar.refetchEvents();
Swal.fire({ icon: 'success', title: `Đã xóa ${count} buổi!`, timer: 1200, showConfirmButton: false });
}

function clearOldData() {
if (!isAdmin) return;
const now = new Date();
const oldCount = getStoredBookings().filter(e => new Date(e.end) {
if (res.isConfirmed) {
saveBookings(getStoredBookings().filter(e => new Date(e.end) > now));
calendar.refetchEvents();
Swal.fire('Đã xóa!', `Đã dọn dẹp ${oldCount} buổi cũ.`, 'success');
}
});
}

// ============================================================
// BỘ LỌC & THỐNG KÊ & XUẤT CSV
// ============================================================
function switchRoom() {
currentRoomId = document.getElementById('roomFilter').value;
document.getElementById('statCurrentRoom').innerText =
document.getElementById('roomFilter').selectedOptions[0].text;
calendar.refetchEvents();
updateStatistics();
}

function filterDepartment() {
currentDeptFilter = document.getElementById('deptFilter').value;
calendar.refetchEvents();
updateStatistics();
}

function updateStatistics() {
if (!calendar) return;
const view = calendar.view;
const startView = view.activeStart;
const endView = view.activeEnd;

const all = getStoredBookings().filter(e => {
const evStart = new Date(e.start);
return e.roomId === currentRoomId &&
evStart >= startView && evStart {
totalHours += (new Date(e.end) - new Date(e.start)) / 3600000;
totalStudents += parseInt(e.studentCount || 0, 10);
});

document.getElementById('statTotalView').innerText = all.length;
document.getElementById('statHoursView').innerText = totalHours.toFixed(1) + 'h';
document.getElementById('statStudentsView').innerText = totalStudents + ' SV';
}

function exportToCSV() {
const bookings = getStoredBookings();
if (!bookings.length) {
Swal.fire('Thông báo', 'Chưa có dữ liệu!', 'info');
return;
}

let csv = "\uFEFFMã,Nhóm,Phòng,Bắt Đầu,Kết Thúc,Người Đặt,Khoa,SĐT,Số SV,Mục Đích\n";
bookings.forEach(b => {
const row = [
b.id, b.groupId || '', ROOM_MAP[b.roomId] || b.roomId, b.start, b.end,
b.bookerName, b.department, b.phone, b.studentCount, b.purpose
].map(v => `"${String(v ?? '').replace(/"/g, '""')}"`).join(',');
csv += row + "\n";
});

const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `Lich_Phong_May_${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}

BÌNH LUẬN

Please enter your comment!
Please enter your name here